Skip to content

Commit c2e3c3b

Browse files
committed
Support impersonating other users for super admin
1 parent e8867a0 commit c2e3c3b

File tree

6 files changed

+176
-12
lines changed

6 files changed

+176
-12
lines changed

application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java

-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
2626
import run.halo.app.core.user.service.RoleService;
2727
import run.halo.app.core.user.service.UserService;
28-
import run.halo.app.extension.ReactiveExtensionClient;
2928
import run.halo.app.infra.AnonymousUserConst;
3029
import run.halo.app.infra.properties.HaloProperties;
3130
import run.halo.app.security.DefaultUserDetailService;
@@ -51,11 +50,8 @@ public class WebServerSecurityConfig {
5150

5251
@Bean
5352
SecurityWebFilterChain filterChain(ServerHttpSecurity http,
54-
RoleService roleService,
5553
ObjectProvider<SecurityConfigurer> securityConfigurers,
5654
ServerSecurityContextRepository securityContextRepository,
57-
ReactiveExtensionClient client,
58-
CryptoService cryptoService,
5955
HaloProperties haloProperties,
6056
ServerRequestCache serverRequestCache) {
6157

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package run.halo.app.security;
2+
3+
import static run.halo.app.security.HaloServerRequestCache.uriInApplication;
4+
5+
import java.net.URI;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.security.core.Authentication;
8+
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
9+
import org.springframework.security.web.server.ServerRedirectStrategy;
10+
import org.springframework.security.web.server.WebFilterExchange;
11+
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
12+
import reactor.core.publisher.Mono;
13+
14+
/**
15+
* This class is responsible for handling the redirection after a successful authentication.
16+
* It checks if a valid 'redirect_uri' query parameter is present in the request. If it is,
17+
* the user is redirected to the specified URI. Otherwise, the user is redirected to a default
18+
* location.
19+
*
20+
* @author johnniang
21+
*/
22+
@Slf4j
23+
public class HaloRedirectAuthenticationSuccessHandler
24+
implements ServerAuthenticationSuccessHandler {
25+
26+
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
27+
28+
private final URI location;
29+
30+
public HaloRedirectAuthenticationSuccessHandler(String location) {
31+
this.location = URI.create(location);
32+
}
33+
34+
@Override
35+
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
36+
Authentication authentication) {
37+
var request = webFilterExchange.getExchange().getRequest();
38+
var redirectUriQuery = request.getQueryParams()
39+
.getFirst("redirect_uri");
40+
if (redirectUriQuery == null || redirectUriQuery.isBlank()) {
41+
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), location);
42+
}
43+
var redirectUri = uriInApplication(request, URI.create(redirectUriQuery));
44+
if (log.isDebugEnabled()) {
45+
log.debug(
46+
"Redirecting to: {} after switching to {}",
47+
redirectUri, authentication.getName()
48+
);
49+
}
50+
return redirectStrategy.sendRedirect(
51+
webFilterExchange.getExchange(), URI.create(redirectUri)
52+
);
53+
}
54+
}

application/src/main/java/run/halo/app/security/HaloServerRequestCache.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ private Mono<Void> saveRedirectUri(ServerWebExchange exchange, URI redirectUri)
8787
.then();
8888
}
8989

90-
private static String uriInApplication(ServerHttpRequest request, URI uri) {
90+
public static String uriInApplication(ServerHttpRequest request, URI uri) {
9191
return uriInApplication(request, uri, true);
9292
}
9393

94-
private static String uriInApplication(
94+
public static String uriInApplication(
9595
ServerHttpRequest request, URI uri, boolean appendFragment
9696
) {
9797
var path = RequestPath.parse(uri, request.getPath().contextPath().value());

application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
1010
import org.springframework.security.authorization.AuthorizationDecision;
1111
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
1213
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
1314
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
1415
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
@@ -55,12 +56,18 @@ SecurityConfigurer unauthenticatedAuthorizationConfigurer() {
5556
@Bean
5657
@Order(200)
5758
SecurityConfigurer preAuthenticationAuthorizationConfigurer() {
58-
return http -> http.authorizeExchange(spec -> spec.pathMatchers(
59-
"/login/**",
60-
"/challenges/**",
61-
"/password-reset/**",
62-
"/signup"
63-
).permitAll());
59+
return http -> http.authorizeExchange(spec -> spec
60+
.pathMatchers("/login/impersonate")
61+
.hasRole(AuthorityUtils.SUPER_ROLE_NAME)
62+
.pathMatchers("/logout/impersonate")
63+
.hasAuthority(SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR)
64+
.pathMatchers(
65+
"/login/**",
66+
"/challenges/**",
67+
"/password-reset/**",
68+
"/signup"
69+
)
70+
.permitAll());
6471
}
6572

6673
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package run.halo.app.security.switchuser;
2+
3+
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
4+
import org.springframework.security.config.web.server.ServerHttpSecurity;
5+
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
6+
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
7+
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
8+
import org.springframework.stereotype.Component;
9+
import run.halo.app.security.HaloRedirectAuthenticationSuccessHandler;
10+
import run.halo.app.security.authentication.SecurityConfigurer;
11+
12+
/**
13+
* Switch user configurer.
14+
*
15+
* @author johnniang
16+
*/
17+
@Component
18+
class SwitchUserConfigurer implements SecurityConfigurer {
19+
20+
private final ReactiveUserDetailsService userDetailsService;
21+
22+
SwitchUserConfigurer(ReactiveUserDetailsService userDetailsService) {
23+
this.userDetailsService = userDetailsService;
24+
}
25+
26+
@Override
27+
public void configure(ServerHttpSecurity http) {
28+
var successHandler = new HaloRedirectAuthenticationSuccessHandler("/console");
29+
var failureHandler =
30+
new RedirectServerAuthenticationFailureHandler("/login?error=impersonate");
31+
var filter = new SwitchUserWebFilter(userDetailsService, successHandler, failureHandler);
32+
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package run.halo.app.security.switchuser;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
import static org.mockito.Mockito.when;
5+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
6+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
7+
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
13+
import org.springframework.security.core.userdetails.User;
14+
import org.springframework.security.test.context.support.WithMockUser;
15+
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
16+
import org.springframework.test.web.reactive.server.WebTestClient;
17+
import reactor.core.publisher.Mono;
18+
import run.halo.app.security.authorization.AuthorityUtils;
19+
20+
21+
@SpringBootTest
22+
@AutoConfigureWebTestClient
23+
class SwitchUserConfigurerTest {
24+
25+
@Autowired
26+
WebTestClient webClient;
27+
28+
@MockitoSpyBean
29+
ReactiveUserDetailsService userDetailsService;
30+
31+
@Test
32+
void shouldSwitchWithSuperRole() {
33+
when(userDetailsService.findByUsername("faker"))
34+
.thenReturn(Mono.fromSupplier(() -> User.withUsername("faker")
35+
.password("password")
36+
.roles("user")
37+
.build()));
38+
var result = webClient.mutateWith(csrf())
39+
.mutateWith(mockUser("admin").roles(AuthorityUtils.SUPER_ROLE_NAME))
40+
.post()
41+
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
42+
"faker", "/fake-success"
43+
)
44+
.exchange()
45+
.expectStatus().isFound()
46+
.expectHeader().location("/fake-success")
47+
.expectCookie().exists("SESSION")
48+
.expectBody().returnResult();
49+
var session = result.getResponseCookies().getFirst("SESSION");
50+
assertNotNull(session);
51+
52+
webClient.mutateWith(csrf())
53+
.post().uri("/logout/impersonate?redirect_uri={redirect_uri}", "/fake-logout-success")
54+
.cookie(session.getName(), session.getValue())
55+
.exchange()
56+
.expectStatus().isFound()
57+
.expectHeader().location("/fake-logout-success");
58+
}
59+
60+
@Test
61+
@WithMockUser(username = "admin", roles = "non-super-role")
62+
void shouldNotSwitchWithoutSuperRole() {
63+
webClient.mutateWith(csrf())
64+
.post()
65+
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
66+
"faker", "/fake-success"
67+
)
68+
.exchange()
69+
.expectStatus().isForbidden();
70+
}
71+
72+
}

0 commit comments

Comments
 (0)