diff --git a/src/main/java/run/halo/app/config/WebSecurityConfig.java b/src/main/java/run/halo/app/config/WebSecurityConfig.java index 044d56341..81efdc3eb 100644 --- a/src/main/java/run/halo/app/config/WebSecurityConfig.java +++ b/src/main/java/run/halo/app/config/WebSecurityConfig.java @@ -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 rules = List.of( diff --git a/src/main/java/run/halo/app/identity/HealthyController.java b/src/main/java/run/halo/app/identity/HealthyController.java new file mode 100644 index 000000000..9543a3fa4 --- /dev/null +++ b/src/main/java/run/halo/app/identity/HealthyController.java @@ -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!')"; + } +} diff --git a/src/main/java/run/halo/app/identity/HelloController.java b/src/main/java/run/halo/app/identity/HelloController.java deleted file mode 100644 index 350faac40..000000000 --- a/src/main/java/run/halo/app/identity/HelloController.java +++ /dev/null @@ -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."; - } -} diff --git a/src/main/java/run/halo/app/identity/TagController.java b/src/main/java/run/halo/app/identity/TagController.java deleted file mode 100644 index 169f9d905..000000000 --- a/src/main/java/run/halo/app/identity/TagController.java +++ /dev/null @@ -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."; - } -} diff --git a/src/main/java/run/halo/app/identity/TestController.java b/src/main/java/run/halo/app/identity/TestController.java new file mode 100644 index 000000000..307b37210 --- /dev/null +++ b/src/main/java/run/halo/app/identity/TestController.java @@ -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"; + } +} diff --git a/src/main/java/run/halo/app/identity/authorization/AuthorizationFilter.java b/src/main/java/run/halo/app/identity/authorization/RequestInfoAuthorizationManager.java similarity index 51% rename from src/main/java/run/halo/app/identity/authorization/AuthorizationFilter.java rename to src/main/java/run/halo/app/identity/authorization/RequestInfoAuthorizationManager.java index bb7fe9140..7cd39fff2 100644 --- a/src/main/java/run/halo/app/identity/authorization/AuthorizationFilter.java +++ b/src/main/java/run/halo/app/identity/authorization/RequestInfoAuthorizationManager.java @@ -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 { + 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 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 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); + } + } diff --git a/src/test/java/run/halo/app/integration/security/AuthorizationFilterTest.java b/src/test/java/run/halo/app/integration/security/RequestInfoAuthorizationManagerTest.java similarity index 80% rename from src/test/java/run/halo/app/integration/security/AuthorizationFilterTest.java rename to src/test/java/run/halo/app/integration/security/RequestInfoAuthorizationManagerTest.java index 62bb489fa..36ce7547a 100644 --- a/src/test/java/run/halo/app/integration/security/AuthorizationFilterTest.java +++ b/src/test/java/run/halo/app/integration/security/RequestInfoAuthorizationManagerTest.java @@ -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; }"; + } } } diff --git a/src/test/java/run/halo/app/integration/security/TestWebSecurityConfig.java b/src/test/java/run/halo/app/integration/security/TestWebSecurityConfig.java index bb39e201b..00efe7b30 100644 --- a/src/test/java/run/halo/app/integration/security/TestWebSecurityConfig.java +++ b/src/test/java/run/halo/app/integration/security/TestWebSecurityConfig.java @@ -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 rules = List.of(