diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 59f8926dfa..cf1b34c442 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -70,6 +70,10 @@ http.csrf((csrf) -> csrf.spa()); * Made so that SLO still returns `` even when validation fails * Removed Open SAML 4 support; applications should migrate to Open SAML 5 +== Test + +* https://github.com/spring-projects/spring-security/issues/17974[Add SecurityMockMvcResultMatchers.withAuthorities(String...)] + == Web * Removed `MvcRequestMatcher` and `AntPathRequestMatcher` in favor of `PathPatternRequestMatcher` diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java index c046a25607..c668a665d3 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java @@ -17,6 +17,7 @@ package org.springframework.security.test.web.servlet.response; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.Consumer; @@ -38,6 +39,7 @@ import org.springframework.test.util.AssertionErrors; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.util.Assert; /** * Security related {@link MockMvc} {@link ResultMatcher}s. @@ -96,6 +98,8 @@ public final class SecurityMockMvcResultMatchers { private @Nullable Collection expectedGrantedAuthorities; + private @Nullable Collection expectedAuthorities; + private Predicate ignoreAuthorities = (authority) -> false; private @Nullable Consumer assertAuthentication; @@ -145,6 +149,20 @@ public final class SecurityMockMvcResultMatchers { this.expectedGrantedAuthorities + " does not contain the same authorities as " + authorities, this.expectedGrantedAuthorities.containsAll(authorities)); } + if (this.expectedAuthorities != null) { + AssertionErrors.assertTrue("Authentication cannot be null", auth != null); + List authorities = auth.getAuthorities() + .stream() + .filter(Predicate.not(this.ignoreAuthorities)) + .map(GrantedAuthority::getAuthority) + .toList(); + AssertionErrors.assertTrue( + authorities + " does not contain the same authorities as " + this.expectedAuthorities, + this.expectedAuthorities.containsAll(authorities)); + AssertionErrors.assertTrue( + this.expectedAuthorities + " does not contain the same authorities as " + authorities, + authorities.containsAll(this.expectedAuthorities)); + } } /** @@ -206,6 +224,17 @@ public final class SecurityMockMvcResultMatchers { return this; } + /** + * Specifies the {@link GrantedAuthority#getAuthority()} + * @param authorities the authorityNames + * @return the {@link AuthenticatedMatcher} for further customization + */ + public AuthenticatedMatcher withAuthorities(String... authorities) { + Assert.notNull(authorities, "authorities cannot be null"); + this.expectedAuthorities = Arrays.asList(authorities); + return this; + } + /** * Specifies the {@link Authentication#getAuthorities()} * @param expected the {@link Authentication#getAuthorities()} diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java index 2840230e73..64ba0f431c 100644 --- a/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java +++ b/test/src/test/java/org/springframework/security/test/web/servlet/response/SecurityMockWithAuthoritiesMvcResultMatchersTests.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.GrantedAuthorities; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -53,6 +54,8 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMv @WebAppConfiguration public class SecurityMockWithAuthoritiesMvcResultMatchersTests { + private static final String ROLE_CUSTOM = "ROLE_CUSTOM"; + @Autowired private WebApplicationContext context; @@ -80,6 +83,12 @@ public class SecurityMockWithAuthoritiesMvcResultMatchersTests { () -> this.mockMvc.perform(formLogin()).andExpect(authenticated().withAuthorities(grantedAuthorities))); } + @Test + public void withAuthoritiesStringSupportsCustomAuthority() throws Exception { + this.mockMvc.perform(formLogin().user("custom")) + .andExpect(authenticated().withAuthorities(ROLE_CUSTOM, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)); + } + @Configuration @EnableWebSecurity @EnableWebMvc @@ -89,7 +98,8 @@ public class SecurityMockWithAuthoritiesMvcResultMatchersTests { UserDetailsService userDetailsService() { // @formatter:off UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("ADMIN", "SELLER").build(); - return new InMemoryUserDetailsManager(user); + UserDetails customAuthorityUser = User.withDefaultPasswordEncoder().username("custom").password("password").authorities(new CustomAuthority(ROLE_CUSTOM)).build(); + return new InMemoryUserDetailsManager(user, customAuthorityUser); // @formatter:on } @@ -105,4 +115,25 @@ public class SecurityMockWithAuthoritiesMvcResultMatchersTests { } + /** + * A custom {@link GrantedAuthority} for testing. + * + * @author Rob Winch + * @since 7.0 + */ + static class CustomAuthority implements GrantedAuthority { + + private final String authority; + + CustomAuthority(String authority) { + this.authority = authority; + } + + @Override + public String getAuthority() { + return this.authority; + } + + } + }