mirror of https://github.com/halo-dev/halo
refactor: remove authorization filter and replace it with the RequestInfoAuthorizationManager (#2081)
parent
6db4f1105a
commit
b4e9b54ffe
|
@ -44,8 +44,8 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
|
|||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
|
||||
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authorization.AuthorizationFilter;
|
||||
import run.halo.app.identity.authorization.PolicyRule;
|
||||
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.identity.authorization.Role;
|
||||
import run.halo.app.identity.authorization.RoleBinding;
|
||||
import run.halo.app.identity.authorization.RoleRef;
|
||||
|
@ -83,7 +83,9 @@ public class WebSecurityConfig {
|
|||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.antMatchers(providerSettings.getTokenEndpoint()).permitAll()
|
||||
.antMatchers("/api/**", "/apis/**").authenticated()
|
||||
.antMatchers("/static/js/**").permitAll()
|
||||
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
|
||||
.anyRequest().access(requestInfoAuthorizationManager())
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
|
@ -93,7 +95,6 @@ public class WebSecurityConfig {
|
|||
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
|
||||
BasicAuthenticationFilter.class)
|
||||
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
|
||||
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
|
||||
.sessionManagement(
|
||||
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
|
@ -103,10 +104,10 @@ public class WebSecurityConfig {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
public AuthorizationFilter authorizationFilter() {
|
||||
public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
|
||||
// TODO fake role and role bindings, only used for testing/development
|
||||
// It'll be deleted next time
|
||||
return new AuthorizationFilter(name -> {
|
||||
return new RequestInfoAuthorizationManager(name -> {
|
||||
// role getter
|
||||
Role role = new Role();
|
||||
List<PolicyRule> rules = List.of(
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package run.halo.app.identity;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* A controller should ONLY be used during testing for this PR.
|
||||
* TODO It'll be deleted next time
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Controller
|
||||
@ResponseBody
|
||||
@RequestMapping
|
||||
public class HealthyController {
|
||||
|
||||
@RequestMapping("/healthy")
|
||||
public String hello() {
|
||||
return "I am very healthy.";
|
||||
}
|
||||
|
||||
@GetMapping("/static/js/test.js")
|
||||
public String fakeJs() {
|
||||
return "console.log('hello world!')";
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package run.halo.app.identity;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* A controller should ONLY be used during testing for this PR.
|
||||
* TODO It'll be deleted next time
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/posts")
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping
|
||||
public String hello() {
|
||||
return "Now you see me.";
|
||||
}
|
||||
|
||||
@GetMapping("/{name}")
|
||||
public String getByName(@PathVariable String name) {
|
||||
return "Name:" + name + "-->Now you see me.";
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package run.halo.app.identity;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* A controller should ONLY be used during testing for this PR.
|
||||
* TODO It'll be deleted next time
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/tags")
|
||||
public class TagController {
|
||||
|
||||
@GetMapping
|
||||
public String hello() {
|
||||
return "Tag you see me.";
|
||||
}
|
||||
|
||||
@GetMapping("/{name}")
|
||||
public String getByName(@PathVariable String name) {
|
||||
return "Tag name:" + name + "-->Now you see me.";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package run.halo.app.identity;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* A controller should ONLY be used during testing for this PR.
|
||||
* TODO It'll be deleted next time
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping
|
||||
public class TestController {
|
||||
|
||||
@GetMapping("/posts")
|
||||
public String hello() {
|
||||
return "list posts.";
|
||||
}
|
||||
|
||||
@GetMapping("/posts/{name}")
|
||||
public String getPostByName(@PathVariable String name) {
|
||||
return "Gets a post with the name: " + name;
|
||||
}
|
||||
|
||||
@GetMapping("/categories/{name}")
|
||||
public String getCategoryByName(@PathVariable String name) {
|
||||
return "Gets a category with the name: " + name;
|
||||
}
|
||||
|
||||
@GetMapping("/categories")
|
||||
public String listCategories() {
|
||||
return "list categories.";
|
||||
}
|
||||
|
||||
@GetMapping("/tags")
|
||||
public String tags() {
|
||||
return "list tags";
|
||||
}
|
||||
|
||||
@GetMapping("/{name}")
|
||||
public String getByName(@PathVariable String name) {
|
||||
return "Gets a tag with the name: " + name;
|
||||
}
|
||||
|
||||
@RequestMapping("/healthy")
|
||||
public String healthy() {
|
||||
return "That's very healthy";
|
||||
}
|
||||
}
|
|
@ -1,46 +1,43 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* An authorization filter that restricts access to the URL.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class AuthorizationFilter extends OncePerRequestFilter {
|
||||
private AuthorizationRuleResolver ruleResolver;
|
||||
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
|
||||
public class RequestInfoAuthorizationManager
|
||||
implements AuthorizationManager<RequestAuthorizationContext> {
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
public AuthorizationFilter(RoleGetter roleGetter, RoleBindingLister roleBindingLister) {
|
||||
private AuthorizationRuleResolver ruleResolver;
|
||||
|
||||
public RequestInfoAuthorizationManager(RoleGetter roleGetter,
|
||||
RoleBindingLister roleBindingLister) {
|
||||
this.ruleResolver = new DefaultRuleResolver(roleGetter, roleBindingLister);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
|
||||
RequestAuthorizationContext requestContext) {
|
||||
HttpServletRequest request = requestContext.getRequest();
|
||||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||
Authentication authentication = getAuthentication();
|
||||
|
||||
Authentication authentication = authenticationSupplier.get();
|
||||
UserDetails userDetails = createUserDetails(authentication);
|
||||
|
||||
AttributesRecord attributes = new AttributesRecord(userDetails, requestInfo);
|
||||
|
@ -52,13 +49,10 @@ public class AuthorizationFilter extends OncePerRequestFilter {
|
|||
if (!authorizingVisitor.isAllowed()) {
|
||||
// print errors
|
||||
showErrorMessage(authorizingVisitor.getErrors());
|
||||
// handle it
|
||||
accessDeniedHandler.handle(request, response,
|
||||
new AccessDeniedException("Access is denied"));
|
||||
return;
|
||||
return new AuthorizationDecision(false);
|
||||
}
|
||||
log.debug(authorizingVisitor.getReason());
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
return new AuthorizationDecision(isGranted(authentication));
|
||||
}
|
||||
|
||||
private void showErrorMessage(List<Throwable> errors) {
|
||||
|
@ -70,15 +64,6 @@ public class AuthorizationFilter extends OncePerRequestFilter {
|
|||
}
|
||||
}
|
||||
|
||||
private Authentication getAuthentication() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
private UserDetails createUserDetails(Authentication authentication) {
|
||||
Assert.notNull(authentication, "The authentication must not be null.");
|
||||
return User.withUsername(authentication.getName())
|
||||
|
@ -92,7 +77,13 @@ public class AuthorizationFilter extends OncePerRequestFilter {
|
|||
this.ruleResolver = ruleResolver;
|
||||
}
|
||||
|
||||
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
|
||||
this.accessDeniedHandler = accessDeniedHandler;
|
||||
private boolean isGranted(Authentication authentication) {
|
||||
return authentication != null && isNotAnonymous(authentication)
|
||||
&& authentication.isAuthenticated();
|
||||
}
|
||||
|
||||
private boolean isNotAnonymous(Authentication authentication) {
|
||||
return !this.trustResolver.isAnonymous(authentication);
|
||||
}
|
||||
|
||||
}
|
|
@ -17,15 +17,15 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import run.halo.app.identity.authorization.AuthorizationFilter;
|
||||
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizationFilter}.
|
||||
* Tests for {@link RequestInfoAuthorizationManager}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class AuthorizationFilterTest extends AuthorizationTestSuit {
|
||||
public class RequestInfoAuthorizationManagerTest extends AuthorizationTestSuit {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
|
@ -94,6 +94,22 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
|
|||
.andExpect(content().string("ok."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cssResourceCanAlwaysAccess() throws Exception {
|
||||
mockMvc.perform(get("/static/test.css"))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().string("body { text-align: center; }"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsResourceCanAlwaysAccess() throws Exception {
|
||||
mockMvc.perform(get("/static/test.js"))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().string("console.log('hello world!')"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonResourceRequestWhenHaveNoRightShould403() throws Exception {
|
||||
mockMvc.perform(get("/healthy/halo")
|
||||
|
@ -133,20 +149,29 @@ public class AuthorizationFilterTest extends AuthorizationTestSuit {
|
|||
}
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/healthy")
|
||||
@ResponseBody
|
||||
@RequestMapping
|
||||
public static class HealthyController {
|
||||
|
||||
@GetMapping
|
||||
@ResponseBody
|
||||
@GetMapping("/healthy")
|
||||
public String check() {
|
||||
return "ok.";
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/{name}")
|
||||
@GetMapping("/healthy/{name}")
|
||||
public String check(@PathVariable String name) {
|
||||
return name + ": should not be seen.";
|
||||
}
|
||||
|
||||
@GetMapping("/static/test.js")
|
||||
public String fakeJs() {
|
||||
return "console.log('hello world!')";
|
||||
}
|
||||
|
||||
@GetMapping("/static/test.css")
|
||||
public String fakeCss() {
|
||||
return "body { text-align: center; }";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -46,8 +46,8 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
|
|||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
|
||||
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authorization.AuthorizationFilter;
|
||||
import run.halo.app.identity.authorization.PolicyRule;
|
||||
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.identity.authorization.Role;
|
||||
import run.halo.app.identity.authorization.RoleBinding;
|
||||
import run.halo.app.identity.authorization.RoleRef;
|
||||
|
@ -85,7 +85,9 @@ public class TestWebSecurityConfig {
|
|||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.antMatchers(providerSettings.getTokenEndpoint()).permitAll()
|
||||
.antMatchers("/api/**", "/apis/**").authenticated()
|
||||
.antMatchers("/static/**").permitAll()
|
||||
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
|
||||
.anyRequest().access(requestInfoAuthorizationManager())
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
|
@ -95,14 +97,13 @@ public class TestWebSecurityConfig {
|
|||
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
|
||||
BasicAuthenticationFilter.class)
|
||||
.addFilterAfter(providerContextFilter, SecurityContextPersistenceFilter.class)
|
||||
.addFilterBefore(authorizationFilter(), FilterSecurityInterceptor.class)
|
||||
.sessionManagement(
|
||||
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
public AuthorizationFilter authorizationFilter() {
|
||||
return new AuthorizationFilter(name -> {
|
||||
public RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
|
||||
return new RequestInfoAuthorizationManager(name -> {
|
||||
// role getter
|
||||
Role role = new Role();
|
||||
List<PolicyRule> rules = List.of(
|
||||
|
|
Loading…
Reference in New Issue