mirror of https://github.com/halo-dev/halo
Support impersonating other users for super admin (#7351)
#### What type of PR is this? /kind feature /area core /milestone 2.20.x #### What this PR does / why we need it: This PR adds support for impersonating other users for super admin. 1. Login as super admin 2. Request `POST /login/impersonate?username=xxx` and the current user should be xxx 3. Request `POST /logout/impersonate` and the current user should be super admin #### Does this PR introduce a user-facing change? ```release-note None ```pull/7353/head^2
parent
482436b2d0
commit
e0b9c50d71
|
@ -25,7 +25,6 @@ import org.springframework.session.MapSession;
|
|||
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.core.user.service.UserService;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.security.DefaultUserDetailService;
|
||||
|
@ -51,11 +50,8 @@ public class WebServerSecurityConfig {
|
|||
|
||||
@Bean
|
||||
SecurityWebFilterChain filterChain(ServerHttpSecurity http,
|
||||
RoleService roleService,
|
||||
ObjectProvider<SecurityConfigurer> securityConfigurers,
|
||||
ServerSecurityContextRepository securityContextRepository,
|
||||
ReactiveExtensionClient client,
|
||||
CryptoService cryptoService,
|
||||
HaloProperties haloProperties,
|
||||
ServerRequestCache serverRequestCache) {
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import static run.halo.app.security.HaloServerRequestCache.uriInApplication;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the redirection after a successful authentication.
|
||||
* It checks if a valid 'redirect_uri' query parameter is present in the request. If it is,
|
||||
* the user is redirected to the specified URI. Otherwise, the user is redirected to a default
|
||||
* location.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class HaloRedirectAuthenticationSuccessHandler
|
||||
implements ServerAuthenticationSuccessHandler {
|
||||
|
||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||
|
||||
private final URI location;
|
||||
|
||||
public HaloRedirectAuthenticationSuccessHandler(String location) {
|
||||
this.location = URI.create(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
|
||||
Authentication authentication) {
|
||||
var request = webFilterExchange.getExchange().getRequest();
|
||||
var redirectUriQuery = request.getQueryParams()
|
||||
.getFirst("redirect_uri");
|
||||
if (redirectUriQuery == null || redirectUriQuery.isBlank()) {
|
||||
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), location);
|
||||
}
|
||||
var redirectUri = uriInApplication(request, URI.create(redirectUriQuery));
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"Redirecting to: {} after switching to {}",
|
||||
redirectUri, authentication.getName()
|
||||
);
|
||||
}
|
||||
return redirectStrategy.sendRedirect(
|
||||
webFilterExchange.getExchange(), URI.create(redirectUri)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -87,11 +87,11 @@ public class HaloServerRequestCache extends WebSessionServerRequestCache {
|
|||
.then();
|
||||
}
|
||||
|
||||
private static String uriInApplication(ServerHttpRequest request, URI uri) {
|
||||
public static String uriInApplication(ServerHttpRequest request, URI uri) {
|
||||
return uriInApplication(request, uri, true);
|
||||
}
|
||||
|
||||
private static String uriInApplication(
|
||||
public static String uriInApplication(
|
||||
ServerHttpRequest request, URI uri, boolean appendFragment
|
||||
) {
|
||||
var path = RequestPath.parse(uri, request.getPath().contextPath().value());
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.springframework.security.authentication.AuthenticationTrustResolver;
|
|||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
|
||||
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
|
||||
|
@ -55,12 +56,18 @@ class AuthorizationExchangeConfigurers {
|
|||
@Bean
|
||||
@Order(200)
|
||||
SecurityConfigurer preAuthenticationAuthorizationConfigurer() {
|
||||
return http -> http.authorizeExchange(spec -> spec.pathMatchers(
|
||||
return http -> http.authorizeExchange(spec -> spec
|
||||
.pathMatchers("/login/impersonate")
|
||||
.hasRole(AuthorityUtils.SUPER_ROLE_NAME)
|
||||
.pathMatchers("/logout/impersonate")
|
||||
.hasAuthority(SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR)
|
||||
.pathMatchers(
|
||||
"/login/**",
|
||||
"/challenges/**",
|
||||
"/password-reset/**",
|
||||
"/signup"
|
||||
).permitAll());
|
||||
)
|
||||
.permitAll());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package run.halo.app.security.switchuser;
|
||||
|
||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.security.HaloRedirectAuthenticationSuccessHandler;
|
||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||
|
||||
/**
|
||||
* Switch user configurer.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Component
|
||||
class SwitchUserConfigurer implements SecurityConfigurer {
|
||||
|
||||
private final ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
SwitchUserConfigurer(ReactiveUserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(ServerHttpSecurity http) {
|
||||
var successHandler = new HaloRedirectAuthenticationSuccessHandler("/console");
|
||||
var failureHandler =
|
||||
new RedirectServerAuthenticationFailureHandler("/login?error=impersonate");
|
||||
var filter = new SwitchUserWebFilter(userDetailsService, successHandler, failureHandler);
|
||||
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package run.halo.app.security.switchuser;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
class SwitchUserConfigurerTest {
|
||||
|
||||
@Autowired
|
||||
WebTestClient webClient;
|
||||
|
||||
@MockitoSpyBean
|
||||
ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
@Test
|
||||
void shouldSwitchWithSuperRole() {
|
||||
when(userDetailsService.findByUsername("faker"))
|
||||
.thenReturn(Mono.fromSupplier(() -> User.withUsername("faker")
|
||||
.password("password")
|
||||
.roles("user")
|
||||
.build()));
|
||||
var result = webClient.mutateWith(csrf())
|
||||
.mutateWith(mockUser("admin").roles(AuthorityUtils.SUPER_ROLE_NAME))
|
||||
.post()
|
||||
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
|
||||
"faker", "/fake-success"
|
||||
)
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/fake-success")
|
||||
.expectCookie().exists("SESSION")
|
||||
.expectBody().returnResult();
|
||||
var session = result.getResponseCookies().getFirst("SESSION");
|
||||
assertNotNull(session);
|
||||
|
||||
webClient.mutateWith(csrf())
|
||||
.post().uri("/logout/impersonate?redirect_uri={redirect_uri}", "/fake-logout-success")
|
||||
.cookie(session.getName(), session.getValue())
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/fake-logout-success");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin", roles = "non-super-role")
|
||||
void shouldNotSwitchWithoutSuperRole() {
|
||||
webClient.mutateWith(csrf())
|
||||
.post()
|
||||
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
|
||||
"faker", "/fake-success"
|
||||
)
|
||||
.exchange()
|
||||
.expectStatus().isForbidden();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue