Reactive SwitchUserWebFilter for user impersonation
Closes gh-8599
This commit is contained in:
parent
0a2006ebec
commit
b22c50c4a8
|
@ -0,0 +1,385 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2020 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.security.web.server.authentication;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
|
||||||
|
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||||
|
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
|
||||||
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
|
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||||
|
import org.springframework.security.web.util.UrlUtils;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.server.WebFilter;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch User processing filter responsible for user context switching.
|
||||||
|
* A common use-case for this feature is the ability to allow
|
||||||
|
* higher-authority users (e.g. ROLE_ADMIN) to switch to a regular user (e.g. ROLE_USER).
|
||||||
|
* <p>
|
||||||
|
* This filter assumes that the user performing the switch will be required to be logged
|
||||||
|
* in as normal user (i.e. with a ROLE_ADMIN role). The user will then access a page/controller
|
||||||
|
* that enables the administrator to specify who they wish to become (see <code>switchUserUrl</code>).
|
||||||
|
* <p>
|
||||||
|
* <b>Note: This URL will be required to have appropriate security constraints configured
|
||||||
|
* so that only users of that role can access it (e.g. ROLE_ADMIN).</b>
|
||||||
|
* <p>
|
||||||
|
* On a successful switch, the user's <code>SecurityContext</code> will be updated to
|
||||||
|
* reflect the specified user and will also contain an additional
|
||||||
|
* {@link org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority}
|
||||||
|
* which contains the original user. Before switching, a check will be made on whether the
|
||||||
|
* user is already currently switched, and any current switch will be exited to prevent
|
||||||
|
* "nested" switches.
|
||||||
|
* <p>
|
||||||
|
* To 'exit' from a user context, the user needs to access a URL (see <code>exitUserUrl</code>)
|
||||||
|
* that will switch back to the original user as identified by the <code>ROLE_PREVIOUS_ADMINISTRATOR</code>.
|
||||||
|
* <p>
|
||||||
|
* To configure the Switch User Processing Filter, create a bean definition for the Switch
|
||||||
|
* User processing filter and add to the filterChainProxy. Note that the filter must come
|
||||||
|
* <b>after</b> the {@link org.springframework.security.config.web.server.SecurityWebFiltersOrder#AUTHORIZATION}
|
||||||
|
* in the chain, in order to apply the correct constraints to the <tt>switchUserUrl</tt>. Example:
|
||||||
|
* <pre>
|
||||||
|
* SwitchUserWebFilter filter = new SwitchUserWebFilter(userDetailsService, loginSuccessHandler, failureHandler);
|
||||||
|
* http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Artur Otrzonsek
|
||||||
|
* @see SwitchUserGrantedAuthority
|
||||||
|
* @since 5.4
|
||||||
|
*/
|
||||||
|
public class SwitchUserWebFilter implements WebFilter {
|
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
public static final String SPRING_SECURITY_SWITCH_USERNAME_KEY = "username";
|
||||||
|
public static final String ROLE_PREVIOUS_ADMINISTRATOR = "ROLE_PREVIOUS_ADMINISTRATOR";
|
||||||
|
|
||||||
|
private final ServerAuthenticationSuccessHandler successHandler;
|
||||||
|
private final ServerAuthenticationFailureHandler failureHandler;
|
||||||
|
private final ReactiveUserDetailsService userDetailsService;
|
||||||
|
private final UserDetailsChecker userDetailsChecker;
|
||||||
|
|
||||||
|
private ServerSecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
|
private ServerWebExchangeMatcher switchUserMatcher = createMatcher("/login/impersonate");
|
||||||
|
private ServerWebExchangeMatcher exitUserMatcher = createMatcher("/logout/impersonate");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a filter for the user context switching
|
||||||
|
*
|
||||||
|
* @param userDetailsService The <tt>UserDetailService</tt> which will be used to load
|
||||||
|
* information for the user that is being switched to.
|
||||||
|
* @param successHandler Used to define custom behaviour on a successful switch or exit user.
|
||||||
|
* @param failureHandler Used to define custom behaviour when a switch fails.
|
||||||
|
*/
|
||||||
|
public SwitchUserWebFilter(ReactiveUserDetailsService userDetailsService,
|
||||||
|
ServerAuthenticationSuccessHandler successHandler,
|
||||||
|
@Nullable ServerAuthenticationFailureHandler failureHandler) {
|
||||||
|
Assert.notNull(userDetailsService, "userDetailsService must be specified");
|
||||||
|
Assert.notNull(successHandler, "successHandler must be specified");
|
||||||
|
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.successHandler = successHandler;
|
||||||
|
this.failureHandler = failureHandler;
|
||||||
|
|
||||||
|
this.securityContextRepository = new WebSessionServerSecurityContextRepository();
|
||||||
|
this.userDetailsChecker = new AccountStatusUserDetailsChecker();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a filter for the user context switching
|
||||||
|
*
|
||||||
|
* @param userDetailsService The <tt>UserDetailService</tt> which will be used to load
|
||||||
|
* information for the user that is being switched to.
|
||||||
|
* @param successTargetUrl Sets the URL to go to after a successful switch / exit user request
|
||||||
|
* @param failureTargetUrl The URL to which a user should be redirected if the switch fails
|
||||||
|
*/
|
||||||
|
public SwitchUserWebFilter(ReactiveUserDetailsService userDetailsService,
|
||||||
|
String successTargetUrl, @Nullable String failureTargetUrl) {
|
||||||
|
Assert.notNull(userDetailsService, "userDetailsService must be specified");
|
||||||
|
Assert.notNull(successTargetUrl, "successTargetUrl must be specified");
|
||||||
|
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.successHandler = new RedirectServerAuthenticationSuccessHandler(successTargetUrl);
|
||||||
|
|
||||||
|
if (failureTargetUrl != null) {
|
||||||
|
this.failureHandler = new RedirectServerAuthenticationFailureHandler(failureTargetUrl);
|
||||||
|
} else {
|
||||||
|
this.failureHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.securityContextRepository = new WebSessionServerSecurityContextRepository();
|
||||||
|
this.userDetailsChecker = new AccountStatusUserDetailsChecker();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||||
|
final WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||||
|
|
||||||
|
return switchUser(webFilterExchange)
|
||||||
|
.switchIfEmpty(Mono.defer(() -> exitSwitchUser(webFilterExchange)))
|
||||||
|
.switchIfEmpty(Mono.defer(() -> chain.filter(exchange).then(Mono.empty())))
|
||||||
|
.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
|
||||||
|
.onErrorResume(SwitchUserAuthenticationException.class, exception -> Mono.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to switch to another user.
|
||||||
|
*
|
||||||
|
* @param webFilterExchange The web filter exchange
|
||||||
|
* @return The new <code>Authentication</code> object if successfully switched to
|
||||||
|
* another user, <code>Mono.empty()</code> otherwise.
|
||||||
|
* @throws AuthenticationCredentialsNotFoundException If the target user can not be found by username
|
||||||
|
*/
|
||||||
|
protected Mono<Authentication> switchUser(WebFilterExchange webFilterExchange) {
|
||||||
|
return switchUserMatcher.matches(webFilterExchange.getExchange())
|
||||||
|
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||||
|
.flatMap(matchResult -> ReactiveSecurityContextHolder.getContext())
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.flatMap(currentAuthentication -> {
|
||||||
|
final String username = getUsername(webFilterExchange.getExchange());
|
||||||
|
return attemptSwitchUser(currentAuthentication, username);
|
||||||
|
})
|
||||||
|
.onErrorResume(AuthenticationException.class, e ->
|
||||||
|
onAuthenticationFailure(e, webFilterExchange)
|
||||||
|
.then(Mono.error(new SwitchUserAuthenticationException(e)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to exit from an already switched user.
|
||||||
|
*
|
||||||
|
* @param webFilterExchange The web filter exchange
|
||||||
|
* @return The original <code>Authentication</code> object.
|
||||||
|
* @throws AuthenticationCredentialsNotFoundException If there is no <code>Authentication</code> associated
|
||||||
|
* with this request or the user is not switched.
|
||||||
|
*/
|
||||||
|
protected Mono<Authentication> exitSwitchUser(WebFilterExchange webFilterExchange) {
|
||||||
|
return exitUserMatcher.matches(webFilterExchange.getExchange())
|
||||||
|
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||||
|
.flatMap(matchResult ->
|
||||||
|
ReactiveSecurityContextHolder.getContext()
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.switchIfEmpty(Mono.error(this::noCurrentUserException))
|
||||||
|
)
|
||||||
|
.map(this::attemptExitUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the target user.
|
||||||
|
*
|
||||||
|
* @param exchange The server web exchange
|
||||||
|
* @return the name of the target user.
|
||||||
|
*/
|
||||||
|
protected String getUsername(ServerWebExchange exchange) {
|
||||||
|
return exchange.getRequest().getQueryParams().getFirst(SPRING_SECURITY_SWITCH_USERNAME_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Mono<Authentication> attemptSwitchUser(Authentication currentAuthentication, String userName) {
|
||||||
|
Assert.notNull(userName, "The userName can not be null.");
|
||||||
|
|
||||||
|
if (this.logger.isDebugEnabled()) {
|
||||||
|
this.logger.debug("Attempt to switch to user [" + userName + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDetailsService.findByUsername(userName)
|
||||||
|
.switchIfEmpty(Mono.error(this::noTargetAuthenticationException))
|
||||||
|
.doOnNext(userDetailsChecker::check)
|
||||||
|
.map(userDetails -> createSwitchUserToken(userDetails, currentAuthentication));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Authentication attemptExitUser(Authentication currentAuthentication) {
|
||||||
|
final Optional<Authentication> sourceAuthentication = extractSourceAuthentication(currentAuthentication);
|
||||||
|
|
||||||
|
if (!sourceAuthentication.isPresent()) {
|
||||||
|
this.logger.debug("Could not find original user Authentication object!");
|
||||||
|
throw noOriginalAuthenticationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceAuthentication.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
|
||||||
|
final ServerWebExchange exchange = webFilterExchange.getExchange();
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
|
||||||
|
return securityContextRepository.save(exchange, securityContext)
|
||||||
|
.then(this.successHandler.onAuthenticationSuccess(webFilterExchange, authentication))
|
||||||
|
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> onAuthenticationFailure(AuthenticationException exception,
|
||||||
|
WebFilterExchange webFilterExchange) {
|
||||||
|
return Mono.justOrEmpty(failureHandler)
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
logger.error("Switch User failed", exception);
|
||||||
|
return Mono.error(exception);
|
||||||
|
}))
|
||||||
|
.flatMap(failureHandler -> failureHandler.onAuthenticationFailure(webFilterExchange, exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Authentication createSwitchUserToken(UserDetails targetUser, Authentication currentAuthentication) {
|
||||||
|
final Optional<Authentication> sourceAuthentication = extractSourceAuthentication(currentAuthentication);
|
||||||
|
|
||||||
|
if (sourceAuthentication.isPresent()) {
|
||||||
|
// SEC-1763. Check first if we are already switched.
|
||||||
|
logger.info("Found original switch user granted authority [" + sourceAuthentication.get() + "]");
|
||||||
|
currentAuthentication = sourceAuthentication.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final GrantedAuthority switchAuthority =
|
||||||
|
new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR, currentAuthentication);
|
||||||
|
final Collection<? extends GrantedAuthority> targetUserAuthorities = targetUser.getAuthorities();
|
||||||
|
|
||||||
|
final List<GrantedAuthority> extendedTargetUserAuthorities = new ArrayList<>(targetUserAuthorities);
|
||||||
|
extendedTargetUserAuthorities.add(switchAuthority);
|
||||||
|
|
||||||
|
return new UsernamePasswordAuthenticationToken(
|
||||||
|
targetUser, targetUser.getPassword(), extendedTargetUserAuthorities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the original <code>Authentication</code> object from the current user's
|
||||||
|
* granted authorities. A successfully switched user should have a
|
||||||
|
* <code>SwitchUserGrantedAuthority</code> that contains the original source user
|
||||||
|
* <code>Authentication</code> object.
|
||||||
|
*
|
||||||
|
* @param currentAuthentication The current <code>Authentication</code> object
|
||||||
|
* @return The source user <code>Authentication</code> object or <code>Optional.empty</code>
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
private Optional<Authentication> extractSourceAuthentication(Authentication currentAuthentication) {
|
||||||
|
// iterate over granted authorities and find the 'switch user' authority
|
||||||
|
for (GrantedAuthority authority : currentAuthentication.getAuthorities()) {
|
||||||
|
if (authority instanceof SwitchUserGrantedAuthority) {
|
||||||
|
final SwitchUserGrantedAuthority switchAuthority = (SwitchUserGrantedAuthority) authority;
|
||||||
|
return Optional.of(switchAuthority.getSource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerWebExchangeMatcher createMatcher(String pattern) {
|
||||||
|
return ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationCredentialsNotFoundException noCurrentUserException() {
|
||||||
|
return new AuthenticationCredentialsNotFoundException(
|
||||||
|
"No current user associated with this request"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationCredentialsNotFoundException noOriginalAuthenticationException() {
|
||||||
|
return new AuthenticationCredentialsNotFoundException(
|
||||||
|
"Could not find original Authentication object"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationCredentialsNotFoundException noTargetAuthenticationException() {
|
||||||
|
return new AuthenticationCredentialsNotFoundException(
|
||||||
|
"No target user for the given username"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SwitchUserAuthenticationException extends RuntimeException {
|
||||||
|
SwitchUserAuthenticationException(AuthenticationException exception) {
|
||||||
|
super(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the repository for persisting the SecurityContext. Default is {@link WebSessionServerSecurityContextRepository}
|
||||||
|
*
|
||||||
|
* @param securityContextRepository the repository to use
|
||||||
|
*/
|
||||||
|
public void setSecurityContextRepository(
|
||||||
|
ServerSecurityContextRepository securityContextRepository) {
|
||||||
|
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||||
|
this.securityContextRepository = securityContextRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the URL to respond to exit user processing. This is a shortcut for
|
||||||
|
* * {@link #setExitUserMatcher(ServerWebExchangeMatcher)}
|
||||||
|
*
|
||||||
|
* @param exitUserUrl The exit user URL.
|
||||||
|
*/
|
||||||
|
public void setExitUserUrl(String exitUserUrl) {
|
||||||
|
Assert.isTrue(UrlUtils.isValidRedirectUrl(exitUserUrl),
|
||||||
|
"exitUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
this.exitUserMatcher = createMatcher(exitUserUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the matcher to respond to exit user processing.
|
||||||
|
*
|
||||||
|
* @param exitUserMatcher The exit matcher to use
|
||||||
|
*/
|
||||||
|
public void setExitUserMatcher(ServerWebExchangeMatcher exitUserMatcher) {
|
||||||
|
Assert.notNull(exitUserMatcher, "exitUserMatcher cannot be null");
|
||||||
|
this.exitUserMatcher = exitUserMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the URL to respond to switch user processing. This is a shortcut for
|
||||||
|
* {@link #setSwitchUserMatcher(ServerWebExchangeMatcher)}
|
||||||
|
*
|
||||||
|
* @param switchUserUrl The switch user URL.
|
||||||
|
*/
|
||||||
|
public void setSwitchUserUrl(String switchUserUrl) {
|
||||||
|
Assert.isTrue(UrlUtils.isValidRedirectUrl(switchUserUrl),
|
||||||
|
"switchUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
this.switchUserMatcher = createMatcher(switchUserUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the matcher to respond to switch user processing.
|
||||||
|
*
|
||||||
|
* @param switchUserMatcher The switch user matcher.
|
||||||
|
*/
|
||||||
|
public void setSwitchUserMatcher(ServerWebExchangeMatcher switchUserMatcher) {
|
||||||
|
Assert.notNull(switchUserMatcher, "switchUserMatcher cannot be null");
|
||||||
|
this.switchUserMatcher = switchUserMatcher;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,656 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2020 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.security.web.server.authentication;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
|
||||||
|
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
|
||||||
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
|
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.security.core.context.ReactiveSecurityContextHolder.withSecurityContext;
|
||||||
|
import static org.springframework.security.web.server.authentication.SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Artur Otrzonsek
|
||||||
|
*/
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class SwitchUserWebFilterTests {
|
||||||
|
|
||||||
|
private SwitchUserWebFilter switchUserWebFilter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ReactiveUserDetailsService userDetailsService;
|
||||||
|
@Mock
|
||||||
|
ServerAuthenticationSuccessHandler successHandler;
|
||||||
|
@Mock
|
||||||
|
private ServerAuthenticationFailureHandler failureHandler;
|
||||||
|
@Mock
|
||||||
|
private ServerSecurityContextRepository serverSecurityContextRepository;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException exceptionRule = ExpectedException.none();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(userDetailsService, successHandler, failureHandler);
|
||||||
|
switchUserWebFilter.setSecurityContextRepository(serverSecurityContextRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUserWhenRequestNotMatchThenDoesNothing() {
|
||||||
|
// given
|
||||||
|
MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/not/existing"));
|
||||||
|
|
||||||
|
WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain).block();
|
||||||
|
// then
|
||||||
|
verifyNoInteractions(userDetailsService);
|
||||||
|
verifyNoInteractions(successHandler);
|
||||||
|
verifyNoInteractions(failureHandler);
|
||||||
|
verifyNoInteractions(serverSecurityContextRepository);
|
||||||
|
|
||||||
|
verify(chain).filter(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUser() {
|
||||||
|
// given
|
||||||
|
final String targetUsername = "TEST_USERNAME";
|
||||||
|
final UserDetails switchUserDetails = switchUserDetails(targetUsername, true);
|
||||||
|
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate?username={targetUser}", targetUsername));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
|
||||||
|
final Authentication originalAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("principal", "credentials");
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(originalAuthentication);
|
||||||
|
|
||||||
|
when(userDetailsService.findByUsername(targetUsername))
|
||||||
|
.thenReturn(Mono.just(switchUserDetails));
|
||||||
|
when(serverSecurityContextRepository.save(eq(exchange), any(SecurityContext.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
when(successHandler.onAuthenticationSuccess(any(WebFilterExchange.class), any(Authentication.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
// then
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
verify(userDetailsService).findByUsername(targetUsername);
|
||||||
|
|
||||||
|
final ArgumentCaptor<SecurityContext> securityContextCaptor = ArgumentCaptor.forClass(SecurityContext.class);
|
||||||
|
verify(serverSecurityContextRepository).save(eq(exchange), securityContextCaptor.capture());
|
||||||
|
final SecurityContext savedSecurityContext = securityContextCaptor.getValue();
|
||||||
|
|
||||||
|
final ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
|
||||||
|
verify(successHandler).onAuthenticationSuccess(any(WebFilterExchange.class), authenticationCaptor.capture());
|
||||||
|
|
||||||
|
final Authentication switchUserAuthentication = authenticationCaptor.getValue();
|
||||||
|
|
||||||
|
assertSame(savedSecurityContext.getAuthentication(), switchUserAuthentication);
|
||||||
|
|
||||||
|
assertEquals("username should point to the switched user",
|
||||||
|
targetUsername, switchUserAuthentication.getName());
|
||||||
|
assertTrue("switchAuthentication should contain SwitchUserGrantedAuthority",
|
||||||
|
switchUserAuthentication.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> a instanceof SwitchUserGrantedAuthority)
|
||||||
|
);
|
||||||
|
assertTrue("new authentication should get new role ",
|
||||||
|
switchUserAuthentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.anyMatch(a -> a.equals(ROLE_PREVIOUS_ADMINISTRATOR))
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
"SwitchUserGrantedAuthority should contain the original authentication",
|
||||||
|
originalAuthentication.getName(),
|
||||||
|
switchUserAuthentication.getAuthorities().stream()
|
||||||
|
.filter(a -> a instanceof SwitchUserGrantedAuthority)
|
||||||
|
.map(a -> ((SwitchUserGrantedAuthority) a).getSource())
|
||||||
|
.map(Principal::getName)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUserWhenUserAlreadySwitchedThenExitSwitchAndSwitchAgain() {
|
||||||
|
// given
|
||||||
|
final Authentication originalAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("origPrincipal", "origCredentials");
|
||||||
|
|
||||||
|
final GrantedAuthority switchAuthority =
|
||||||
|
new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR, originalAuthentication);
|
||||||
|
final Authentication switchUserAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("switchPrincipal", "switchCredentials",
|
||||||
|
Collections.singleton(switchAuthority));
|
||||||
|
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(switchUserAuthentication);
|
||||||
|
|
||||||
|
final String targetUsername = "newSwitchPrincipal";
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate?username={targetUser}", targetUsername));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
|
||||||
|
when(serverSecurityContextRepository.save(eq(exchange), any(SecurityContext.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
when(successHandler.onAuthenticationSuccess(any(WebFilterExchange.class), any(Authentication.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
when(userDetailsService.findByUsername(targetUsername))
|
||||||
|
.thenReturn(Mono.just(switchUserDetails(targetUsername, true)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
// then
|
||||||
|
final ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
|
||||||
|
verify(successHandler).onAuthenticationSuccess(any(WebFilterExchange.class), authenticationCaptor.capture());
|
||||||
|
|
||||||
|
final Authentication secondSwitchUserAuthentication = authenticationCaptor.getValue();
|
||||||
|
|
||||||
|
assertEquals("username should point to the switched user",
|
||||||
|
targetUsername, secondSwitchUserAuthentication.getName());
|
||||||
|
assertEquals(
|
||||||
|
"SwitchUserGrantedAuthority should contain the original authentication",
|
||||||
|
originalAuthentication.getName(),
|
||||||
|
secondSwitchUserAuthentication.getAuthorities().stream()
|
||||||
|
.filter(a -> a instanceof SwitchUserGrantedAuthority)
|
||||||
|
.map(a -> ((SwitchUserGrantedAuthority) a).getSource())
|
||||||
|
.map(Principal::getName)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUserWhenUsernameIsMissingThenThrowException() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate"));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(mock(Authentication.class));
|
||||||
|
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("The userName can not be null.");
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUserWhenExceptionThenCallFailureHandler() {
|
||||||
|
final String targetUsername = "TEST_USERNAME";
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate?username={targetUser}", targetUsername));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(mock(Authentication.class));
|
||||||
|
|
||||||
|
final UserDetails switchUserDetails = switchUserDetails(targetUsername, false);
|
||||||
|
when(userDetailsService.findByUsername(any(String.class))).thenReturn(Mono.just(switchUserDetails));
|
||||||
|
when(failureHandler.onAuthenticationFailure(any(WebFilterExchange.class), any(DisabledException.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(failureHandler).onAuthenticationFailure(any(WebFilterExchange.class), any(DisabledException.class));
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void switchUserWhenFailureHandlerNotDefinedThenReturnError() {
|
||||||
|
// given
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(userDetailsService, successHandler, null);
|
||||||
|
|
||||||
|
final String targetUsername = "TEST_USERNAME";
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate?username={targetUser}", targetUsername));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(mock(Authentication.class));
|
||||||
|
|
||||||
|
final UserDetails switchUserDetails = switchUserDetails(targetUsername, false);
|
||||||
|
when(userDetailsService.findByUsername(any(String.class))).thenReturn(Mono.just(switchUserDetails));
|
||||||
|
|
||||||
|
exceptionRule.expect(DisabledException.class);
|
||||||
|
|
||||||
|
// when then
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exitSwitchThenReturnToOriginalAuthentication() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/logout/impersonate"));
|
||||||
|
|
||||||
|
final Authentication originalAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("origPrincipal", "origCredentials");
|
||||||
|
|
||||||
|
final GrantedAuthority switchAuthority =
|
||||||
|
new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR, originalAuthentication);
|
||||||
|
final Authentication switchUserAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("switchPrincipal", "switchCredentials",
|
||||||
|
Collections.singleton(switchAuthority));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(switchUserAuthentication);
|
||||||
|
|
||||||
|
when(serverSecurityContextRepository.save(eq(exchange), any(SecurityContext.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
when(successHandler.onAuthenticationSuccess(any(WebFilterExchange.class), any(Authentication.class)))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
// then
|
||||||
|
final ArgumentCaptor<SecurityContext> securityContextCaptor = ArgumentCaptor.forClass(SecurityContext.class);
|
||||||
|
verify(serverSecurityContextRepository).save(eq(exchange), securityContextCaptor.capture());
|
||||||
|
final SecurityContext savedSecurityContext = securityContextCaptor.getValue();
|
||||||
|
|
||||||
|
final ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
|
||||||
|
verify(successHandler).onAuthenticationSuccess(any(WebFilterExchange.class), authenticationCaptor.capture());
|
||||||
|
|
||||||
|
final Authentication originalAuthenticationValue = authenticationCaptor.getValue();
|
||||||
|
|
||||||
|
assertSame(originalAuthentication, savedSecurityContext.getAuthentication());
|
||||||
|
assertSame(originalAuthentication, originalAuthenticationValue);
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exitSwitchWhenUserNotSwitchedThenThrowError() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/logout/impersonate"));
|
||||||
|
|
||||||
|
final Authentication originalAuthentication =
|
||||||
|
new UsernamePasswordAuthenticationToken("origPrincipal", "origCredentials");
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
final SecurityContextImpl securityContext = new SecurityContextImpl(originalAuthentication);
|
||||||
|
|
||||||
|
exceptionRule.expect(AuthenticationCredentialsNotFoundException.class);
|
||||||
|
exceptionRule.expectMessage("Could not find original Authentication object");
|
||||||
|
|
||||||
|
// when then
|
||||||
|
switchUserWebFilter.filter(exchange, chain)
|
||||||
|
.subscriberContext(withSecurityContext(Mono.just(securityContext)))
|
||||||
|
.block();
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exitSwitchWhenNoCurrentUserThenThrowError() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/logout/impersonate"));
|
||||||
|
|
||||||
|
final WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
|
||||||
|
exceptionRule.expect(AuthenticationCredentialsNotFoundException.class);
|
||||||
|
exceptionRule.expectMessage("No current user associated with this request");
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.filter(exchange, chain).block();
|
||||||
|
//then
|
||||||
|
verifyNoInteractions(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorUserDetailsServiceRequired() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("userDetailsService must be specified");
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(
|
||||||
|
null,
|
||||||
|
mock(ServerAuthenticationSuccessHandler.class),
|
||||||
|
mock(ServerAuthenticationFailureHandler.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorServerAuthenticationSuccessHandlerRequired() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("successHandler must be specified");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(
|
||||||
|
mock(ReactiveUserDetailsService.class),
|
||||||
|
null,
|
||||||
|
mock(ServerAuthenticationFailureHandler.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorSuccessTargetUrlRequired() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("successTargetUrl must be specified");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(
|
||||||
|
mock(ReactiveUserDetailsService.class),
|
||||||
|
null,
|
||||||
|
"failure/target/url"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorFirstDefaultValues() {
|
||||||
|
// when
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(
|
||||||
|
mock(ReactiveUserDetailsService.class),
|
||||||
|
mock(ServerAuthenticationSuccessHandler.class),
|
||||||
|
mock(ServerAuthenticationFailureHandler.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
final Object securityContextRepository =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "securityContextRepository");
|
||||||
|
assertTrue(securityContextRepository instanceof WebSessionServerSecurityContextRepository);
|
||||||
|
|
||||||
|
final Object userDetailsChecker =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "userDetailsChecker");
|
||||||
|
assertTrue(userDetailsChecker instanceof AccountStatusUserDetailsChecker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorSecondDefaultValues() {
|
||||||
|
// when
|
||||||
|
switchUserWebFilter = new SwitchUserWebFilter(
|
||||||
|
mock(ReactiveUserDetailsService.class),
|
||||||
|
"success/target/url",
|
||||||
|
"failure/target/url"
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
final Object successHandler =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "successHandler");
|
||||||
|
assertTrue(successHandler instanceof RedirectServerAuthenticationSuccessHandler);
|
||||||
|
|
||||||
|
final Object failureHandler =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "failureHandler");
|
||||||
|
assertTrue(failureHandler instanceof RedirectServerAuthenticationFailureHandler);
|
||||||
|
|
||||||
|
final Object securityContextRepository =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "securityContextRepository");
|
||||||
|
assertTrue(securityContextRepository instanceof WebSessionServerSecurityContextRepository);
|
||||||
|
|
||||||
|
final Object userDetailsChecker =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "userDetailsChecker");
|
||||||
|
assertTrue(userDetailsChecker instanceof AccountStatusUserDetailsChecker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSecurityContextRepositoryWhenNullThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("securityContextRepository cannot be null");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSecurityContextRepository(null);
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSecurityContextRepositoryWhenDefinedThenChangeDefaultValue() {
|
||||||
|
// given
|
||||||
|
final Object oldSecurityContextRepository =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "securityContextRepository");
|
||||||
|
assertSame(serverSecurityContextRepository, oldSecurityContextRepository);
|
||||||
|
|
||||||
|
final ServerSecurityContextRepository newSecurityContextRepository = mock(ServerSecurityContextRepository.class);
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSecurityContextRepository(newSecurityContextRepository);
|
||||||
|
// then
|
||||||
|
final Object currentSecurityContextRepository =
|
||||||
|
ReflectionTestUtils.getField(switchUserWebFilter, "securityContextRepository");
|
||||||
|
assertSame(newSecurityContextRepository, currentSecurityContextRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setExitUserUrlWhenNullThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("exitUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setExitUserUrl(null);
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setExitUserUrlWhenInvalidUrlThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("exitUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setExitUserUrl("wrongUrl");
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setExitUserUrlWhenDefinedThenChangeDefaultValue() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/logout/impersonate"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher oldExitUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "exitUserMatcher");
|
||||||
|
|
||||||
|
assertThat(oldExitUserMatcher.matches(exchange).block().isMatch()).isTrue();
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setExitUserUrl("/exit-url");
|
||||||
|
|
||||||
|
// then
|
||||||
|
final MockServerWebExchange newExchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/exit-url"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher newExitUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "exitUserMatcher");
|
||||||
|
|
||||||
|
assertThat(newExitUserMatcher.matches(newExchange).block().isMatch()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setExitUserMatcherWhenNullThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("exitUserMatcher cannot be null");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setExitUserMatcher(null);
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setExitUserMatcherWhenDefinedThenChangeDefaultValue() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/logout/impersonate"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher oldExitUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "exitUserMatcher");
|
||||||
|
|
||||||
|
assertThat(oldExitUserMatcher.matches(exchange).block().isMatch()).isTrue();
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher newExitUserMatcher =
|
||||||
|
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/exit-url");
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setExitUserMatcher(newExitUserMatcher);
|
||||||
|
|
||||||
|
// then
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher currentExitUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "exitUserMatcher");
|
||||||
|
|
||||||
|
assertSame(newExitUserMatcher, currentExitUserMatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSwitchUserUrlWhenNullThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("switchUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSwitchUserUrl(null);
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSwitchUserUrlWhenInvalidThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("switchUserUrl cannot be empty and must be a valid redirect URL");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSwitchUserUrl("wrongUrl");
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSwitchUserUrlWhenDefinedThenChangeDefaultValue() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher oldSwitchUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "switchUserMatcher");
|
||||||
|
|
||||||
|
assertThat(oldSwitchUserMatcher.matches(exchange).block().isMatch()).isTrue();
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSwitchUserUrl("/switch-url");
|
||||||
|
|
||||||
|
// then
|
||||||
|
final MockServerWebExchange newExchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/switch-url"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher newSwitchUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "switchUserMatcher");
|
||||||
|
|
||||||
|
assertThat(newSwitchUserMatcher.matches(newExchange).block().isMatch()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSwitchUserMatcherWhenNullThenThrowException() {
|
||||||
|
// given
|
||||||
|
exceptionRule.expect(IllegalArgumentException.class);
|
||||||
|
exceptionRule.expectMessage("switchUserMatcher cannot be null");
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSwitchUserMatcher(null);
|
||||||
|
// then
|
||||||
|
fail("Test should fail with exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void setSwitchUserMatcherWhenDefinedThenChangeDefaultValue() {
|
||||||
|
// given
|
||||||
|
final MockServerWebExchange exchange = MockServerWebExchange
|
||||||
|
.from(MockServerHttpRequest.post("/login/impersonate"));
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher oldSwitchUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "switchUserMatcher");
|
||||||
|
|
||||||
|
assertThat(oldSwitchUserMatcher.matches(exchange).block().isMatch()).isTrue();
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher newSwitchUserMatcher =
|
||||||
|
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/switch-url");
|
||||||
|
|
||||||
|
// when
|
||||||
|
switchUserWebFilter.setSwitchUserMatcher(newSwitchUserMatcher);
|
||||||
|
|
||||||
|
// then
|
||||||
|
|
||||||
|
final ServerWebExchangeMatcher currentExitUserMatcher =
|
||||||
|
(ServerWebExchangeMatcher) ReflectionTestUtils.getField(switchUserWebFilter, "switchUserMatcher");
|
||||||
|
|
||||||
|
assertSame(newSwitchUserMatcher, currentExitUserMatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDetails switchUserDetails(String username, boolean enabled) {
|
||||||
|
final SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_SWITCH_TEST");
|
||||||
|
return new User(username, "NA", enabled,
|
||||||
|
true, true, true, Collections.singleton(authority));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue