diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 88e3eae23b..cee2c912ec 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -37,8 +37,10 @@ import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar; import org.springframework.security.aot.hint.SecurityHintsRegistrar; +import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactory; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -121,6 +123,16 @@ void setRoleHierarchy(RoleHierarchy roleHierarchy) { this.expressionHandler.setRoleHierarchy(roleHierarchy); } + @Autowired(required = false) + void setTrustResolver(AuthenticationTrustResolver trustResolver) { + this.expressionHandler.setTrustResolver(trustResolver); + } + + @Autowired(required = false) + void setAuthorizationManagerFactory(AuthorizationManagerFactory authorizationManagerFactory) { + this.expressionHandler.setAuthorizationManagerFactory(authorizationManagerFactory); + } + @Autowired(required = false) void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { this.preFilterMethodInterceptor.setTemplateDefaults(templateDefaults); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 38858ecd95..c995f9427f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.function.Function; -import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; @@ -27,13 +26,12 @@ import org.springframework.core.ResolvableType; import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.authorization.AuthenticatedAuthorizationManager; -import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactory; import org.springframework.security.authorization.AuthorizationManagers; -import org.springframework.security.authorization.SingleResultAuthorizationManager; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; @@ -46,7 +44,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; -import org.springframework.util.function.SingletonSupplier; /** * Adds a URL based authorization using {@link AuthorizationManager}. @@ -62,9 +59,7 @@ public final class AuthorizeHttpRequestsConfigurer roleHierarchy; - - private String rolePrefix = "ROLE_"; + private final AuthorizationManagerFactory authorizationManagerFactory; private ObjectPostProcessor> postProcessor = ObjectPostProcessor .identity(); @@ -81,13 +76,7 @@ public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { else { this.publisher = new SpringAuthorizationEventPublisher(context); } - this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0) - ? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy()); - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length > 0) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } + this.authorizationManagerFactory = getAuthorizationManagerFactory(context); ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, ResolvableType.forClassWithGenerics(AuthorizationManager.class, HttpServletRequest.class)); ObjectProvider>> provider = context @@ -95,6 +84,28 @@ public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { provider.ifUnique((postProcessor) -> this.postProcessor = postProcessor); } + private AuthorizationManagerFactory getAuthorizationManagerFactory( + ApplicationContext context) { + ResolvableType authorizationManagerFactoryType = ResolvableType + .forClassWithGenerics(AuthorizationManagerFactory.class, RequestAuthorizationContext.class); + ObjectProvider> authorizationManagerFactoryProvider = context + .getBeanProvider(authorizationManagerFactoryType); + + return authorizationManagerFactoryProvider.getIfAvailable(() -> { + RoleHierarchy roleHierarchy = context.getBeanProvider(RoleHierarchy.class) + .getIfAvailable(NullRoleHierarchy::new); + GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBeanProvider(GrantedAuthorityDefaults.class) + .getIfAvailable(); + String rolePrefix = (grantedAuthorityDefaults != null) ? grantedAuthorityDefaults.getRolePrefix() : "ROLE_"; + + DefaultAuthorizationManagerFactory authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>(); + authorizationManagerFactory.setRoleHierarchy(roleHierarchy); + authorizationManagerFactory.setRolePrefix(rolePrefix); + + return authorizationManagerFactory; + }); + } + /** * The {@link AuthorizationManagerRequestMatcherRegistry} is what users will interact * with after applying the {@link AuthorizeHttpRequestsConfigurer}. @@ -173,7 +184,7 @@ private AuthorizationManager createAuthorizationManager() { @Override protected AuthorizedUrl chainRequestMatchers(List requestMatchers) { this.unmappedMatchers = requestMatchers; - return new AuthorizedUrl(requestMatchers); + return new AuthorizedUrl(requestMatchers, AuthorizeHttpRequestsConfigurer.this.authorizationManagerFactory); } /** @@ -201,20 +212,31 @@ public class AuthorizedUrl { private final List matchers; + private AuthorizationManagerFactory authorizationManagerFactory; + private boolean not; /** * Creates an instance. * @param matchers the {@link RequestMatcher} instances to map + * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} for + * creating instances of {@link AuthorizationManager} */ - AuthorizedUrl(List matchers) { + AuthorizedUrl(List matchers, + AuthorizationManagerFactory authorizationManagerFactory) { this.matchers = matchers; + this.authorizationManagerFactory = authorizationManagerFactory; } protected List getMatchers() { return this.matchers; } + void setAuthorizationManagerFactory( + AuthorizationManagerFactory authorizationManagerFactory) { + this.authorizationManagerFactory = authorizationManagerFactory; + } + /** * Negates the following authorization rule. * @return the {@link AuthorizedUrl} for further customization @@ -231,7 +253,7 @@ public AuthorizedUrl not() { * customizations */ public AuthorizationManagerRequestMatcherRegistry permitAll() { - return access(SingleResultAuthorizationManager.permitAll()); + return access(this.authorizationManagerFactory.permitAll()); } /** @@ -240,7 +262,7 @@ public AuthorizationManagerRequestMatcherRegistry permitAll() { * customizations */ public AuthorizationManagerRequestMatcherRegistry denyAll() { - return access(SingleResultAuthorizationManager.denyAll()); + return access(this.authorizationManagerFactory.denyAll()); } /** @@ -251,8 +273,7 @@ public AuthorizationManagerRequestMatcherRegistry denyAll() { * customizations */ public AuthorizationManagerRequestMatcherRegistry hasRole(String role) { - return access(withRoleHierarchy(AuthorityAuthorizationManager - .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role }))); + return access(this.authorizationManagerFactory.hasRole(role)); } /** @@ -264,8 +285,7 @@ public AuthorizationManagerRequestMatcherRegistry hasRole(String role) { * customizations */ public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) { - return access(withRoleHierarchy( - AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles))); + return access(this.authorizationManagerFactory.hasAnyRole(roles)); } /** @@ -275,7 +295,7 @@ public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) { * customizations */ public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) { - return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority))); + return access(this.authorizationManagerFactory.hasAuthority(authority)); } /** @@ -286,13 +306,7 @@ public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) * customizations */ public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) { - return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities))); - } - - private AuthorityAuthorizationManager withRoleHierarchy( - AuthorityAuthorizationManager manager) { - manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get()); - return manager; + return access(this.authorizationManagerFactory.hasAnyAuthority(authorities)); } /** @@ -301,7 +315,7 @@ private AuthorityAuthorizationManager withRoleHiera * customizations */ public AuthorizationManagerRequestMatcherRegistry authenticated() { - return access(AuthenticatedAuthorizationManager.authenticated()); + return access(this.authorizationManagerFactory.authenticated()); } /** @@ -313,7 +327,7 @@ public AuthorizationManagerRequestMatcherRegistry authenticated() { * @see RememberMeConfigurer */ public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() { - return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); + return access(this.authorizationManagerFactory.fullyAuthenticated()); } /** @@ -324,7 +338,7 @@ public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() { * @see RememberMeConfigurer */ public AuthorizationManagerRequestMatcherRegistry rememberMe() { - return access(AuthenticatedAuthorizationManager.rememberMe()); + return access(this.authorizationManagerFactory.rememberMe()); } /** @@ -334,7 +348,7 @@ public AuthorizationManagerRequestMatcherRegistry rememberMe() { * @since 5.8 */ public AuthorizationManagerRequestMatcherRegistry anonymous() { - return access(AuthenticatedAuthorizationManager.anonymous()); + return access(this.authorizationManagerFactory.anonymous()); } /** diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index eaaf268eeb..e550586f3a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.Set; import java.util.function.Supplier; import io.micrometer.observation.Observation; @@ -36,14 +37,19 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; +import org.springframework.http.HttpMethod; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactory; import org.springframework.security.authorization.AuthorizationObservationContext; +import org.springframework.security.authorization.SingleResultAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.authorization.event.AuthorizationDeniedEvent; import org.springframework.security.config.ObjectPostProcessor; @@ -82,13 +88,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -170,6 +180,26 @@ public void configureMvcMatcherAccessAuthorizationManagerWhenNullThenException() .withMessageContaining("manager cannot be null"); } + @Test + public void configureWhenCustomAuthorizationManagerFactoryRegisteredThenUsed() { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + this.spring.register(AuthorizationManagerFactoryConfig.class).autowire(); + verify(authorizationManagerFactory).permitAll(); + verify(authorizationManagerFactory).denyAll(); + verify(authorizationManagerFactory).hasRole("ADMIN"); + verify(authorizationManagerFactory).hasAnyRole("USER", "ADMIN"); + verify(authorizationManagerFactory).hasAuthority("write"); + verify(authorizationManagerFactory).hasAnyAuthority("resource.read", "read"); + verify(authorizationManagerFactory).authenticated(); + verify(authorizationManagerFactory).fullyAuthenticated(); + verify(authorizationManagerFactory).rememberMe(); + verify(authorizationManagerFactory).anonymous(); + verifyNoMoreInteractions(authorizationManagerFactory); + } + @Test public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); @@ -538,6 +568,205 @@ public void getWhenCustomRolePrefixAndHasAnyRoleThenRespondsWithOk() throws Exce this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); } + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndPermitAllThenRespondsWithOk() throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager permitAll = spy(SingleResultAuthorizationManager.permitAll()); + given(authorizationManagerFactory.permitAll()).willReturn(permitAll); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/public").with(anonymous()); + this.mvc.perform(request).andExpect(status().isOk()); + verify(permitAll).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(permitAll); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndDenyAllThenRespondsWithForbidden() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager denyAll = spy(SingleResultAuthorizationManager.denyAll()); + given(authorizationManagerFactory.denyAll()).willReturn(denyAll); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/private").with(user("user")); + this.mvc.perform(request).andExpect(status().isForbidden()); + verify(denyAll).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(denyAll); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasRoleThenRespondsWithOk() throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager hasRole = spy(AuthorityAuthorizationManager.hasRole("ADMIN")); + given(authorizationManagerFactory.hasRole(anyString())).willReturn(hasRole); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/admin").with(user("admin").roles("ADMIN")); + this.mvc.perform(request).andExpect(status().isOk()); + verify(hasRole).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(hasRole); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasAnyRoleThenRespondsWithOk() throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager hasAnyRole = spy( + AuthorityAuthorizationManager.hasAnyRole("USER", "ADMIN")); + given(authorizationManagerFactory.hasAnyRole(any(String[].class))).willReturn(hasAnyRole); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/user").with(user("user").roles("USER")); + this.mvc.perform(request).andExpect(status().isOk()); + verify(hasAnyRole).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(hasAnyRole); + verifyNoInteractions(authorizationManager); + } + + @Test + public void postWhenCustomAuthorizationManagerFactoryRegisteredAndHasAuthorityThenRespondsWithOk() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager hasAuthority = spy( + AuthorityAuthorizationManager.hasAuthority("write")); + given(authorizationManagerFactory.hasAuthority(anyString())).willReturn(hasAuthority); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = post("/resource") + .with(user("user").authorities(new SimpleGrantedAuthority("write"))) + .with(csrf()); + this.mvc.perform(request).andExpect(status().isOk()); + verify(hasAuthority).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(hasAuthority); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndHasAnyAuthorityThenRespondsWithOk() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager hasAnyAuthority = spy( + AuthorityAuthorizationManager.hasAnyAuthority("resource.read", "read")); + given(authorizationManagerFactory.hasAnyAuthority(any(String[].class))).willReturn(hasAnyAuthority); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/resource") + .with(user("user").authorities(new SimpleGrantedAuthority("read"))); + this.mvc.perform(request).andExpect(status().isOk()); + verify(hasAnyAuthority).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(hasAnyAuthority); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAuthenticatedThenRespondsWithOk() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager authenticated = spy( + AuthenticatedAuthorizationManager.authenticated()); + given(authorizationManagerFactory.authenticated()).willReturn(authenticated); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/authenticated").with(user("user")); + this.mvc.perform(request).andExpect(status().isOk()); + verify(authenticated).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(authenticated); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndFullyAuthenticatedThenRespondsWithOk() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager fullyAuthenticated = spy( + AuthenticatedAuthorizationManager.fullyAuthenticated()); + given(authorizationManagerFactory.fullyAuthenticated()).willReturn(fullyAuthenticated); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/fully-authenticated").with(user("user")); + this.mvc.perform(request).andExpect(status().isOk()); + verify(fullyAuthenticated).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(fullyAuthenticated); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndRememberMeThenRespondsWithOk() throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager rememberMe = spy( + AuthenticatedAuthorizationManager.rememberMe()); + given(authorizationManagerFactory.rememberMe()).willReturn(rememberMe); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/remember-me") + .with(authentication(new RememberMeAuthenticationToken("test", "user", Set.of()))); + this.mvc.perform(request).andExpect(status().isOk()); + verify(rememberMe).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(rememberMe); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAnonymousThenRespondsWithOk() throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactory authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + AuthorizationManager anonymous = spy( + AuthenticatedAuthorizationManager.anonymous()); + given(authorizationManagerFactory.anonymous()).willReturn(anonymous); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = authorizationManagerFactory; + + this.spring.register(AuthorizationManagerFactoryConfig.class, AccessTestController.class).autowire(); + MockHttpServletRequestBuilder request = get("/anonymous").with(anonymous()); + this.mvc.perform(request).andExpect(status().isOk()); + verify(anonymous).authorize(any(), any(RequestAuthorizationContext.class)); + verifyNoMoreInteractions(anonymous); + verifyNoInteractions(authorizationManager); + } + + @Test + public void getWhenCustomAuthorizationManagerFactoryRegisteredAndAccessThenRespondsWithForbidden() + throws Exception { + AuthorizationManager authorizationManager = mock(); + AuthorizationManagerFactoryConfig.authorizationManagerFactory = mockAuthorizationManagerFactory( + authorizationManager); + + this.spring.register(AuthorizationManagerFactoryConfig.class).autowire(); + MockHttpServletRequestBuilder request = get("/").with(user("user")); + this.mvc.perform(request).andExpect(status().isForbidden()); + verifyNoInteractions(authorizationManager); + } + @Test public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk() throws Exception { @@ -587,6 +816,23 @@ private static RequestPostProcessor remoteAddress(String remoteAddress) { }; } + private AuthorizationManagerFactory mockAuthorizationManagerFactory( + AuthorizationManager authorizationManager) { + AuthorizationManagerFactory authorizationManagerFactory = mock(); + given(authorizationManagerFactory.permitAll()).willReturn(authorizationManager); + given(authorizationManagerFactory.denyAll()).willReturn(authorizationManager); + given(authorizationManagerFactory.hasRole(anyString())).willReturn(authorizationManager); + given(authorizationManagerFactory.hasAnyRole(any(String[].class))).willReturn(authorizationManager); + given(authorizationManagerFactory.hasAuthority(anyString())).willReturn(authorizationManager); + given(authorizationManagerFactory.hasAnyAuthority(any(String[].class))).willReturn(authorizationManager); + given(authorizationManagerFactory.authenticated()).willReturn(authorizationManager); + given(authorizationManagerFactory.fullyAuthenticated()).willReturn(authorizationManager); + given(authorizationManagerFactory.rememberMe()).willReturn(authorizationManager); + given(authorizationManagerFactory.anonymous()).willReturn(authorizationManager); + + return authorizationManagerFactory; + } + @Test public void getWhenFullyAuthenticatedConfiguredAndRememberMeTokenThenRespondsWithUnauthorized() throws Exception { this.spring.register(FullyAuthenticatedConfig.class, BasicController.class).autowire(); @@ -850,6 +1096,41 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + static class AuthorizationManagerFactoryConfig { + + static AuthorizationManagerFactory authorizationManagerFactory; + + @Bean + AuthorizationManagerFactory authorizationManagerFactory() { + return authorizationManagerFactory; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/public").permitAll() + .requestMatchers("/private").denyAll() + .requestMatchers("/admin").hasRole("ADMIN") + .requestMatchers("/user").hasAnyRole("USER", "ADMIN") + .requestMatchers(HttpMethod.POST, "/resource").hasAuthority("write") + .requestMatchers("/resource").hasAnyAuthority("resource.read", "read") + .requestMatchers("/authenticated").authenticated() + .requestMatchers("/fully-authenticated").fullyAuthenticated() + .requestMatchers("/remember-me").rememberMe() + .requestMatchers("/anonymous").anonymous() + .anyRequest().access((authentication, context) -> new AuthorizationDecision(false)) + ); + // @formatter:on + + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -1295,6 +1576,47 @@ void path() { } + @RestController + static class AccessTestController { + + @RequestMapping("/public") + void publicEndpoint() { + } + + @RequestMapping("/private") + void privateEndpoint() { + } + + @RequestMapping("/admin") + void adminEndpoint() { + } + + @RequestMapping("/user") + void userEndpoint() { + } + + @RequestMapping("/resource") + void resourceEndpoint() { + } + + @RequestMapping("/authenticated") + void authenticatedEndpoint() { + } + + @RequestMapping("/fully-authenticated") + void fullyAuthenticatedEndpoint() { + } + + @RequestMapping("/remember-me") + void rememberMeEndpoint() { + } + + @RequestMapping("/anonymous") + void anonymousEndpoint() { + } + + } + @Configuration static class ObservationRegistryConfig { diff --git a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java index e710d9442c..b4737f7ce4 100644 --- a/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/AbstractSecurityExpressionHandler.java @@ -28,6 +28,8 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authorization.AuthorizationManagerFactory; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -47,7 +49,7 @@ public abstract class AbstractSecurityExpressionHandler private @Nullable BeanResolver beanResolver; - private @Nullable RoleHierarchy roleHierarchy; + private AuthorizationManagerFactory authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>(); private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); @@ -105,12 +107,58 @@ protected StandardEvaluationContext createEvaluationContextInternal(Authenticati protected abstract SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, T invocation); + /** + * Sets the {@link AuthorizationManagerFactory} to be used. The default is + * {@link DefaultAuthorizationManagerFactory}. + * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use. + * Cannot be null. + * @since 7.0 + */ + public final void setAuthorizationManagerFactory(AuthorizationManagerFactory authorizationManagerFactory) { + Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null"); + this.authorizationManagerFactory = authorizationManagerFactory; + } + + protected final AuthorizationManagerFactory getAuthorizationManagerFactory() { + return this.authorizationManagerFactory; + } + + /** + * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and + * setting defaults. This method will be removed in Spring Security 8. + * @return the {@link DefaultAuthorizationManagerFactory} + * @throws IllegalStateException if a different {@link AuthorizationManagerFactory} + * was already set + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0") + protected final DefaultAuthorizationManagerFactory getDefaultAuthorizationManagerFactory() { + if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory defaultAuthorizationManagerFactory)) { + throw new IllegalStateException( + "authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory"); + } + + return defaultAuthorizationManagerFactory; + } + + /** + * @deprecated Use {@link #getDefaultAuthorizationManagerFactory()} instead + */ + @Deprecated(since = "7.0") protected @Nullable RoleHierarchy getRoleHierarchy() { - return this.roleHierarchy; + return getDefaultAuthorizationManagerFactory().getRoleHierarchy(); } - public void setRoleHierarchy(RoleHierarchy roleHierarchy) { - this.roleHierarchy = roleHierarchy; + /** + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0") + public void setRoleHierarchy(@Nullable RoleHierarchy roleHierarchy) { + if (roleHierarchy != null) { + getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy); + } } protected PermissionEvaluator getPermissionEvaluator() { diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java index c16faece85..7e30c06bc4 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java @@ -17,8 +17,6 @@ package org.springframework.security.access.expression; import java.io.Serializable; -import java.util.Collection; -import java.util.Set; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -26,10 +24,11 @@ import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactory; +import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; @@ -38,19 +37,16 @@ * * @author Luke Taylor * @author Evgeniy Cheban + * @author Steve Riesenberg * @since 3.0 */ -public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { +public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { private final Supplier authentication; - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + private final T object; - private @Nullable RoleHierarchy roleHierarchy; - - private @Nullable Set roles; - - private String defaultRolePrefix = "ROLE_"; + private AuthorizationManagerFactory authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>(); /** * Allows "permitAll" expression @@ -77,9 +73,12 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat /** * Creates a new instance * @param authentication the {@link Authentication} to use. Cannot be null. + * @deprecated Use {@link #SecurityExpressionRoot(Supplier, Object)} instead */ + @Deprecated(since = "7.0") + @SuppressWarnings("NullAway") public SecurityExpressionRoot(Authentication authentication) { - this(() -> authentication); + this(() -> authentication, null); } /** @@ -88,23 +87,39 @@ public SecurityExpressionRoot(Authentication authentication) { * @param authentication the {@link Supplier} of the {@link Authentication} to use. * Cannot be null. * @since 5.8 + * @deprecated Use {@link #SecurityExpressionRoot(Supplier, Object)} instead */ + @Deprecated(since = "7.0") + @SuppressWarnings("NullAway") public SecurityExpressionRoot(Supplier authentication) { + this(authentication, null); + } + + /** + * Creates a new instance that uses lazy initialization of the {@link Authentication} + * object. + * @param authentication the {@link Supplier} of the {@link Authentication} to use. + * Cannot be null. + * @param object the object being authorized + * @since 7.0 + */ + public SecurityExpressionRoot(Supplier authentication, T object) { this.authentication = SingletonSupplier.of(() -> { Authentication value = authentication.get(); Assert.notNull(value, "Authentication object cannot be null"); return value; }); + this.object = object; } @Override public final boolean hasAuthority(String authority) { - return hasAnyAuthority(authority); + return isGranted(this.authorizationManagerFactory.hasAnyAuthority(authority)); } @Override public final boolean hasAnyAuthority(String... authorities) { - return hasAnyAuthorityName(null, authorities); + return isGranted(this.authorizationManagerFactory.hasAnyAuthority(authorities)); } @Override @@ -114,18 +129,19 @@ public final boolean hasRole(String role) { @Override public final boolean hasAnyRole(String... roles) { - return hasAnyAuthorityName(this.defaultRolePrefix, roles); - } - - private boolean hasAnyAuthorityName(@Nullable String prefix, String... roles) { - Set roleSet = getAuthoritySet(); - for (String role : roles) { - String defaultedRole = getRoleWithDefaultPrefix(prefix, role); - if (roleSet.contains(defaultedRole)) { - return true; + if (this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory defaultAuthorizationManagerFactory) { + // To provide passivity for old behavior where hasRole('ROLE_A') is allowed, + // we strip the role prefix when found. + // TODO: Remove in favor of fixing inconsistent behavior? + String rolePrefix = defaultAuthorizationManagerFactory.getRolePrefix(); + for (int index = 0; index < roles.length; index++) { + String role = roles[index]; + if (role.startsWith(rolePrefix)) { + roles[index] = role.substring(rolePrefix.length()); + } } } - return false; + return isGranted(this.authorizationManagerFactory.hasAnyRole(roles)); } @Override @@ -135,33 +151,37 @@ public final Authentication getAuthentication() { @Override public final boolean permitAll() { - return true; + return isGranted(this.authorizationManagerFactory.permitAll()); } @Override public final boolean denyAll() { - return false; + return isGranted(this.authorizationManagerFactory.denyAll()); } @Override public final boolean isAnonymous() { - return this.trustResolver.isAnonymous(getAuthentication()); + return isGranted(this.authorizationManagerFactory.anonymous()); } @Override public final boolean isAuthenticated() { - return this.trustResolver.isAuthenticated(getAuthentication()); + return isGranted(this.authorizationManagerFactory.authenticated()); } @Override public final boolean isRememberMe() { - return this.trustResolver.isRememberMe(getAuthentication()); + return isGranted(this.authorizationManagerFactory.rememberMe()); } @Override public final boolean isFullyAuthenticated() { - Authentication authentication = getAuthentication(); - return this.trustResolver.isFullyAuthenticated(authentication); + return isGranted(this.authorizationManagerFactory.fullyAuthenticated()); + } + + private boolean isGranted(AuthorizationManager authorizationManager) { + AuthorizationResult authorizationResult = authorizationManager.authorize(this.authentication, this.object); + return (authorizationResult != null && authorizationResult.isGranted()); } /** @@ -173,12 +193,22 @@ public final boolean isFullyAuthenticated() { return getAuthentication().getPrincipal(); } + /** + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0") public void setTrustResolver(AuthenticationTrustResolver trustResolver) { - this.trustResolver = trustResolver; + getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver); } + /** + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0") public void setRoleHierarchy(RoleHierarchy roleHierarchy) { - this.roleHierarchy = roleHierarchy; + getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy); } /** @@ -193,20 +223,45 @@ public void setRoleHierarchy(RoleHierarchy roleHierarchy) { * If null or empty, then no default role prefix is used. *

* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_". + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ - public void setDefaultRolePrefix(String defaultRolePrefix) { - this.defaultRolePrefix = defaultRolePrefix; + @Deprecated(since = "7.0") + public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) { + if (defaultRolePrefix == null) { + defaultRolePrefix = ""; + } + getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix); } - private Set getAuthoritySet() { - if (this.roles == null) { - Collection userAuthorities = getAuthentication().getAuthorities(); - if (this.roleHierarchy != null) { - userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities); - } - this.roles = AuthorityUtils.authorityListToSet(userAuthorities); + /** + * Sets the {@link AuthorizationManagerFactory} to use for creating instances of + * {@link AuthorizationManager}. + * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use + * @since 7.0 + */ + public void setAuthorizationManagerFactory(AuthorizationManagerFactory authorizationManagerFactory) { + Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null"); + this.authorizationManagerFactory = authorizationManagerFactory; + } + + /** + * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and + * setting defaults. This method will be removed in Spring Security 8. + * @return the {@link DefaultAuthorizationManagerFactory} + * @throws IllegalStateException if a different {@link AuthorizationManagerFactory} + * was already set + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0", forRemoval = true) + private DefaultAuthorizationManagerFactory getDefaultAuthorizationManagerFactory() { + if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory defaultAuthorizationManagerFactory)) { + throw new IllegalStateException( + "authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory"); } - return this.roles; + + return defaultAuthorizationManagerFactory; } @Override @@ -225,24 +280,4 @@ public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } - /** - * Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role - * does not already start with defaultRolePrefix. - * @param defaultRolePrefix - * @param role - * @return - */ - private static String getRoleWithDefaultPrefix(@Nullable String defaultRolePrefix, String role) { - if (role == null) { - return role; - } - if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) { - return role; - } - if (role.startsWith(defaultRolePrefix)) { - return role; - } - return defaultRolePrefix + role; - } - } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 4450aae48d..bb91ce993b 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java @@ -43,6 +43,8 @@ import org.springframework.security.access.expression.ExpressionUtils; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationManagerFactory; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer; import org.springframework.util.Assert; @@ -56,6 +58,7 @@ * @author Luke Taylor * @author Evgeniy Cheban * @author Blagoja Stamatovski + * @author Steve Riesenberg * @since 3.0 */ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler @@ -63,14 +66,10 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr protected final Log logger = LogFactory.getLog(getClass()); - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer(); private @Nullable PermissionCacheOptimizer permissionCacheOptimizer = null; - private String defaultRolePrefix = "ROLE_"; - public DefaultMethodSecurityExpressionHandler() { } @@ -103,12 +102,10 @@ protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authen private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier authentication, MethodInvocation invocation) { - MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication); - root.setThis(invocation.getThis()); + MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication, invocation); + root.setAuthorizationManagerFactory(getAuthorizationManagerFactory()); root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(getTrustResolver()); - Optional.ofNullable(getRoleHierarchy()).ifPresent(root::setRoleHierarchy); - root.setDefaultRolePrefix(getDefaultRolePrefix()); + root.setThis(invocation.getThis()); return root; } @@ -229,17 +226,23 @@ private Object filterStream(final Stream filterTarget, Expression filterExpre * {@link AuthenticationTrustResolverImpl}. * @param trustResolver the {@link AuthenticationTrustResolver} to use. Cannot be * null. + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ + @Deprecated(since = "7.0") public void setTrustResolver(AuthenticationTrustResolver trustResolver) { Assert.notNull(trustResolver, "trustResolver cannot be null"); - this.trustResolver = trustResolver; + getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver); } /** * @return The current {@link AuthenticationTrustResolver} + * @deprecated Use {@link DefaultAuthorizationManagerFactory#getTrustResolver()} + * instead */ + @Deprecated(since = "7.0") protected AuthenticationTrustResolver getTrustResolver() { - return this.trustResolver; + return getDefaultAuthorizationManagerFactory().getTrustResolver(); } /** @@ -286,16 +289,24 @@ public void setReturnObject(@Nullable Object returnObject, EvaluationContext ctx * If null or empty, then no default role prefix is used. *

* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_". + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ - public void setDefaultRolePrefix(String defaultRolePrefix) { - this.defaultRolePrefix = defaultRolePrefix; + @Deprecated(since = "7.0") + public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) { + if (defaultRolePrefix == null) { + defaultRolePrefix = ""; + } + getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix); } /** * @return The default role prefix + * @deprecated Use {@link DefaultAuthorizationManagerFactory#getRolePrefix()} instead */ + @Deprecated(since = "7.0") protected String getDefaultRolePrefix() { - return this.defaultRolePrefix; + return getDefaultAuthorizationManagerFactory().getRolePrefix(); } } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java index 1aba5ba84f..f46173c75e 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRoot.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; +import org.aopalliance.intercept.MethodInvocation; import org.jspecify.annotations.Nullable; import org.springframework.security.access.expression.SecurityExpressionRoot; @@ -28,9 +29,11 @@ * * @author Luke Taylor * @author Evgeniy Cheban + * @author Steve Riesenberg * @since 3.0 */ -class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations { +class MethodSecurityExpressionRoot extends SecurityExpressionRoot + implements MethodSecurityExpressionOperations { private @Nullable Object filterObject; @@ -38,12 +41,8 @@ class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements Met private @Nullable Object target; - MethodSecurityExpressionRoot(Authentication a) { - super(a); - } - - MethodSecurityExpressionRoot(Supplier authentication) { - super(authentication); + MethodSecurityExpressionRoot(Supplier authentication, MethodInvocation methodInvocation) { + super(authentication, methodInvocation); } @Override diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java new file mode 100644 index 0000000000..e342b6228f --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import org.jspecify.annotations.Nullable; + +/** + * A factory for creating different kinds of {@link AuthorizationManager} instances. + * + * @param the type of object that the authorization check is being done on + * @author Steve Riesenberg + * @since 7.0 + */ +public interface AuthorizationManagerFactory { + + /** + * Create an {@link AuthorizationManager} that allows anyone. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager permitAll() { + return SingleResultAuthorizationManager.permitAll(); + } + + /** + * Creates an {@link AuthorizationManager} that does not allow anyone. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager denyAll() { + return SingleResultAuthorizationManager.denyAll(); + } + + /** + * Creates an {@link AuthorizationManager} that requires users to have the specified + * role. + * @param role the role (automatically prepended with ROLE_) that should be required + * to allow access (i.e. USER, ADMIN, etc.) + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager hasRole(String role) { + return AuthorityAuthorizationManager.hasRole(role); + } + + /** + * Creates an {@link AuthorizationManager} that requires users to have one of many + * roles. + * @param roles the roles (automatically prepended with ROLE_) that the user should + * have at least one of to allow access (i.e. USER, ADMIN, etc.) + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager hasAnyRole(String... roles) { + return AuthorityAuthorizationManager.hasAnyRole(roles); + } + + /** + * Creates an {@link AuthorizationManager} that requires users to have the specified + * authority. + * @param authority the authority that should be required to allow access (i.e. USER, + * ADMIN, etc.) + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager hasAuthority(String authority) { + return AuthorityAuthorizationManager.hasAuthority(authority); + } + + /** + * Creates an {@link AuthorizationManager} that requires users to have one of many + * authorities. + * @param authorities the authorities that the user should have at least one of to + * allow access (i.e. ROLE_USER, ROLE_ADMIN, etc.) + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager hasAnyAuthority(String... authorities) { + return AuthorityAuthorizationManager.hasAnyAuthority(authorities); + } + + /** + * Creates an {@link AuthorizationManager} that allows any authenticated user. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager authenticated() { + return AuthenticatedAuthorizationManager.authenticated(); + } + + /** + * Creates an {@link AuthorizationManager} that allows users who have authenticated + * and were not remembered. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager fullyAuthenticated() { + return AuthenticatedAuthorizationManager.fullyAuthenticated(); + } + + /** + * Creates an {@link AuthorizationManager} that allows users that have been + * remembered. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager rememberMe() { + return AuthenticatedAuthorizationManager.rememberMe(); + } + + /** + * Creates an {@link AuthorizationManager} that allows only anonymous users. + * @return A new {@link AuthorizationManager} instance + */ + default AuthorizationManager anonymous() { + return AuthenticatedAuthorizationManager.anonymous(); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java new file mode 100644 index 0000000000..5363b302c0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import org.jspecify.annotations.Nullable; + +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.util.Assert; + +/** + * A factory for creating different kinds of {@link AuthorizationManager} instances. + * + * @param the type of object that the authorization check is being done on + * @author Steve Riesenberg + * @since 7.0 + */ +public final class DefaultAuthorizationManagerFactory + implements AuthorizationManagerFactory { + + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + + private String rolePrefix = "ROLE_"; + + /** + * Returns the {@link AuthenticationTrustResolver} used to check the user's + * authentication. + * @return the {@link AuthenticationTrustResolver} + */ + public AuthenticationTrustResolver getTrustResolver() { + return this.trustResolver; + } + + /** + * Sets the {@link AuthenticationTrustResolver} used to check the user's + * authentication. + * @param trustResolver the {@link AuthenticationTrustResolver} to use + */ + public void setTrustResolver(AuthenticationTrustResolver trustResolver) { + Assert.notNull(trustResolver, "trustResolver cannot be null"); + this.trustResolver = trustResolver; + } + + /** + * Returns the {@link RoleHierarchy} used to discover reachable authorities. + * @return the {@link RoleHierarchy} + */ + public RoleHierarchy getRoleHierarchy() { + return this.roleHierarchy; + } + + /** + * Sets the {@link RoleHierarchy} used to discover reachable authorities. + * @param roleHierarchy the {@link RoleHierarchy} to use + */ + public void setRoleHierarchy(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; + } + + /** + * Returns the prefix used to create an authority name from a role name. + * @return the role prefix + */ + public String getRolePrefix() { + return this.rolePrefix; + } + + /** + * Sets the prefix used to create an authority name from a role name. Can be an empty + * string. + * @param rolePrefix the role prefix to use + */ + public void setRolePrefix(String rolePrefix) { + Assert.notNull(rolePrefix, "rolePrefix cannot be null"); + this.rolePrefix = rolePrefix; + } + + @Override + public AuthorizationManager hasRole(String role) { + return hasAnyRole(role); + } + + @Override + public AuthorizationManager hasAnyRole(String... roles) { + return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)); + } + + @Override + public AuthorizationManager hasAuthority(String authority) { + return withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)); + } + + @Override + public AuthorizationManager hasAnyAuthority(String... authorities) { + return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + } + + @Override + public AuthorizationManager authenticated() { + return withTrustResolver(AuthenticatedAuthorizationManager.authenticated()); + } + + @Override + public AuthorizationManager fullyAuthenticated() { + return withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated()); + } + + @Override + public AuthorizationManager rememberMe() { + return withTrustResolver(AuthenticatedAuthorizationManager.rememberMe()); + } + + @Override + public AuthorizationManager anonymous() { + return withTrustResolver(AuthenticatedAuthorizationManager.anonymous()); + } + + private AuthorityAuthorizationManager withRoleHierarchy(AuthorityAuthorizationManager authorizationManager) { + authorizationManager.setRoleHierarchy(this.roleHierarchy); + return authorizationManager; + } + + private AuthenticatedAuthorizationManager withTrustResolver( + AuthenticatedAuthorizationManager authorizationManager) { + authorizationManager.setTrustResolver(this.trustResolver); + return authorizationManager; + } + +} diff --git a/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java b/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java index 5df9dc7b0a..6f65547e91 100644 --- a/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java +++ b/core/src/test/java/org/springframework/security/access/expression/method/MethodSecurityExpressionRootTests.java @@ -16,6 +16,7 @@ package org.springframework.security.access.expression.method; +import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,7 +54,7 @@ public class MethodSecurityExpressionRootTests { @BeforeEach public void createContext() { this.user = mock(Authentication.class); - this.root = new MethodSecurityExpressionRoot(this.user); + this.root = new MethodSecurityExpressionRoot(() -> this.user, mock(MethodInvocation.class)); this.ctx = new StandardEvaluationContext(); this.ctx.setRootObject(this.root); this.trustResolver = mock(AuthenticationTrustResolver.class); diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java new file mode 100644 index 0000000000..8ea9ed24f5 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AuthorizationManagerFactory}. + * + * @author Steve Riesenberg + */ +public class AuthorizationManagerFactoryTests { + + @Test + public void permitAllReturnsSingleResultAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.permitAll(); + assertThat(authorizationManager).isInstanceOf(SingleResultAuthorizationManager.class); + } + + @Test + public void denyAllReturnsSingleResultAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.denyAll(); + assertThat(authorizationManager).isInstanceOf(SingleResultAuthorizationManager.class); + } + + @Test + public void hasRoleReturnsAuthorityAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasRole("USER"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAnyRoleReturnsAuthorityAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAnyRole("USER", "ADMIN"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAuthorityReturnsAuthorityAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAuthority("authority1"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAnyAuthorityReturnsAuthorityAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAnyAuthority("authority1", "authority2"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void authenticatedReturnsAuthenticatedAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.authenticated(); + assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class); + } + + @Test + public void fullyAuthenticatedReturnsAuthenticatedAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.fullyAuthenticated(); + assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class); + } + + @Test + public void rememberMeReturnsAuthenticatedAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.rememberMe(); + assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class); + } + + @Test + public void anonymousReturnsAuthenticatedAuthorizationManagerByDefault() { + AuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.anonymous(); + assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class); + } + +} diff --git a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java index 4ea5b86950..eb60fadaa3 100644 --- a/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java +++ b/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java @@ -24,6 +24,8 @@ import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationManagerFactory; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -95,14 +97,10 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte private Authentication authentication; - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - - private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + private AuthorizationManagerFactory authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>(); private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator(); - private String defaultRolePrefix = "ROLE_"; - /** * Creates a new instance that uses the current {@link Authentication} found on the * {@link org.springframework.security.core.context.SecurityContextHolder}. @@ -124,14 +122,12 @@ public String getExtensionId() { } @Override - public SecurityExpressionRoot getRootObject() { + public SecurityExpressionRoot getRootObject() { Authentication authentication = getAuthentication(); - SecurityExpressionRoot root = new SecurityExpressionRoot(authentication) { + SecurityExpressionRoot root = new SecurityExpressionRoot<>(() -> authentication, new Object()) { }; - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(this.roleHierarchy); + root.setAuthorizationManagerFactory(this.authorizationManagerFactory); root.setPermissionEvaluator(this.permissionEvaluator); - root.setDefaultRolePrefix(this.defaultRolePrefix); return root; } @@ -154,15 +150,46 @@ private Authentication getAuthentication() { return context.getAuthentication(); } + /** + * Sets the {@link AuthorizationManagerFactory} to be used. The default is + * {@link DefaultAuthorizationManagerFactory}. + * @param authorizationManagerFactory the {@link AuthorizationManagerFactory} to use. + * Cannot be null. + * @since 7.0 + */ + public void setAuthorizationManagerFactory(AuthorizationManagerFactory authorizationManagerFactory) { + Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory cannot be null"); + this.authorizationManagerFactory = authorizationManagerFactory; + } + + /** + * Allows accessing the {@link DefaultAuthorizationManagerFactory} for getting and + * setting defaults. This method will be removed in Spring Security 8. + * @return the {@link DefaultAuthorizationManagerFactory} + * @throws IllegalStateException if a different {@link AuthorizationManagerFactory} + * was already set + */ + private DefaultAuthorizationManagerFactory getDefaultAuthorizationManagerFactory() { + if (!(this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory defaultAuthorizationManagerFactory)) { + throw new IllegalStateException( + "authorizationManagerFactory must be an instance of DefaultAuthorizationManagerFactory"); + } + + return defaultAuthorizationManagerFactory; + } + /** * Sets the {@link AuthenticationTrustResolver} to be used. Default is * {@link AuthenticationTrustResolverImpl}. Cannot be null. * @param trustResolver the {@link AuthenticationTrustResolver} to use * @since 5.8 + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ + @Deprecated(since = "7.0") public void setTrustResolver(AuthenticationTrustResolver trustResolver) { Assert.notNull(trustResolver, "trustResolver cannot be null"); - this.trustResolver = trustResolver; + getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver); } /** @@ -170,10 +197,13 @@ public void setTrustResolver(AuthenticationTrustResolver trustResolver) { * Cannot be null. * @param roleHierarchy the {@link RoleHierarchy} to use * @since 5.8 + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ + @Deprecated(since = "7.0") public void setRoleHierarchy(RoleHierarchy roleHierarchy) { Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); - this.roleHierarchy = roleHierarchy; + getDefaultAuthorizationManagerFactory().setRoleHierarchy(roleHierarchy); } /** @@ -197,9 +227,12 @@ public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { * @param defaultRolePrefix the default prefix to add to roles. The default is * "ROLE_". * @since 5.8 + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ + @Deprecated(since = "7.0") public void setDefaultRolePrefix(String defaultRolePrefix) { - this.defaultRolePrefix = defaultRolePrefix; + getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix); } } diff --git a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java index 8854cdf60d..0eb292dd1c 100644 --- a/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java +++ b/data/src/test/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtensionTests.java @@ -23,10 +23,9 @@ import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.DenyAllPermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionRoot; -import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -102,9 +101,11 @@ public void setTrustResolverWhenNullThenIllegalArgumentException() { public void setTrustResolverWhenNotNullThenVerifyRootObject() { TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); this.securityExtension = new SecurityEvaluationContextExtension(explicit); - AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + AuthenticationTrustResolver trustResolver = mock(AuthenticationTrustResolver.class); + given(trustResolver.isAuthenticated(explicit)).willReturn(true); this.securityExtension.setTrustResolver(trustResolver); - assertThat(getRoot()).extracting("trustResolver").isEqualTo(trustResolver); + assertThat(getRoot().isAuthenticated()).isTrue(); + verify(trustResolver).isAuthenticated(explicit); } @Test @@ -117,11 +118,11 @@ public void setRoleHierarchyWhenNullThenIllegalArgumentException() { @Test public void setRoleHierarchyWhenNotNullThenVerifyRootObject() { - TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_PARENT"); this.securityExtension = new SecurityEvaluationContextExtension(explicit); - RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + RoleHierarchy roleHierarchy = RoleHierarchyImpl.fromHierarchy("ROLE_PARENT > ROLE_EXPLICIT"); this.securityExtension.setRoleHierarchy(roleHierarchy); - assertThat(getRoot()).extracting("roleHierarchy").isEqualTo(roleHierarchy); + assertThat(getRoot().hasRole("EXPLICIT")).isTrue(); } @Test @@ -143,25 +144,25 @@ public void setPermissionEvaluatorWhenNotNullThenVerifyRootObject() { @Test public void setDefaultRolePrefixWhenCustomThenVerifyRootObject() { - TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); + TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "CUSTOM_EXPLICIT"); this.securityExtension = new SecurityEvaluationContextExtension(explicit); String defaultRolePrefix = "CUSTOM_"; this.securityExtension.setDefaultRolePrefix(defaultRolePrefix); - assertThat(getRoot()).extracting("defaultRolePrefix").isEqualTo(defaultRolePrefix); + assertThat(getRoot().hasRole("EXPLICIT")).isTrue(); } @Test public void getRootObjectWhenAdditionalFieldsNotSetThenVerifyDefaults() { TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT"); this.securityExtension = new SecurityEvaluationContextExtension(explicit); - SecurityExpressionRoot root = getRoot(); - assertThat(root).extracting("trustResolver").isInstanceOf(AuthenticationTrustResolverImpl.class); - assertThat(root).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); - assertThat(root).extracting("permissionEvaluator").isInstanceOf(DenyAllPermissionEvaluator.class); - assertThat(root).extracting("defaultRolePrefix").isEqualTo("ROLE_"); + SecurityExpressionRoot securityExpressionRoot = getRoot(); + assertThat(securityExpressionRoot.isAuthenticated()).isTrue(); + assertThat(securityExpressionRoot.hasRole("PARENT")).isFalse(); + assertThat(securityExpressionRoot.hasRole("EXPLICIT")).isTrue(); + assertThat(securityExpressionRoot.hasPermission(new Object(), "read")).isFalse(); } - private SecurityExpressionRoot getRoot() { + private SecurityExpressionRoot getRoot() { return this.securityExtension.getRootObject(); } diff --git a/etc/checkstyle/checkstyle-suppressions.xml b/etc/checkstyle/checkstyle-suppressions.xml index f7c3a56e38..f9434eb1bb 100644 --- a/etc/checkstyle/checkstyle-suppressions.xml +++ b/etc/checkstyle/checkstyle-suppressions.xml @@ -36,6 +36,7 @@ + diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java index fbb14a6191..cbf4eb76ba 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/DefaultMessageSecurityExpressionHandler.java @@ -18,6 +18,8 @@ import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.messaging.Message; @@ -25,9 +27,8 @@ import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.expression.SecurityExpressionOperations; import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationManagerFactory; import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; /** * The default implementation of {@link SecurityExpressionHandler} which uses a @@ -38,13 +39,12 @@ * @author Evgeniy Cheban * @since 4.0 */ +@NullMarked public class DefaultMessageSecurityExpressionHandler extends AbstractSecurityExpressionHandler> { - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - @Override public EvaluationContext createEvaluationContext(Supplier authentication, Message message) { - MessageSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, message); + MessageSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, message); StandardEvaluationContext ctx = new StandardEvaluationContext(root); ctx.setBeanResolver(getBeanResolver()); return ctx; @@ -56,18 +56,21 @@ protected SecurityExpressionOperations createSecurityExpressionRoot(Authenticati return createSecurityExpressionRoot(() -> authentication, invocation); } - private MessageSecurityExpressionRoot createSecurityExpressionRoot(Supplier authentication, + private MessageSecurityExpressionRoot createSecurityExpressionRoot(Supplier authentication, Message invocation) { - MessageSecurityExpressionRoot root = new MessageSecurityExpressionRoot(authentication, invocation); + MessageSecurityExpressionRoot root = new MessageSecurityExpressionRoot<>(authentication, invocation); + root.setAuthorizationManagerFactory(getAuthorizationManagerFactory()); root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(getRoleHierarchy()); return root; } + /** + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead + */ + @Deprecated(since = "7.0") public void setTrustResolver(AuthenticationTrustResolver trustResolver) { - Assert.notNull(trustResolver, "trustResolver cannot be null"); - this.trustResolver = trustResolver; + getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver); } } diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java index 594a9bcbd2..5b37659d4a 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/expression/MessageSecurityExpressionRoot.java @@ -18,6 +18,8 @@ import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; + import org.springframework.messaging.Message; import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.Authentication; @@ -29,11 +31,12 @@ * @author Evgeniy Cheban * @since 4.0 */ -public class MessageSecurityExpressionRoot extends SecurityExpressionRoot { +@NullMarked +public class MessageSecurityExpressionRoot extends SecurityExpressionRoot> { - public final Message message; + public final Message message; - public MessageSecurityExpressionRoot(Authentication authentication, Message message) { + public MessageSecurityExpressionRoot(Authentication authentication, Message message) { this(() -> authentication, message); } @@ -44,8 +47,8 @@ public MessageSecurityExpressionRoot(Authentication authentication, Message m * @param message the {@link Message} to use * @since 5.8 */ - public MessageSecurityExpressionRoot(Supplier authentication, Message message) { - super(authentication); + public MessageSecurityExpressionRoot(Supplier authentication, Message message) { + super(authentication, message); this.message = message; } diff --git a/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java b/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java index 28abd48946..e56b2403eb 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.java @@ -18,6 +18,9 @@ import java.util.function.Supplier; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; @@ -25,9 +28,9 @@ import org.springframework.security.access.expression.SecurityExpressionOperations; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationManagerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; -import org.springframework.util.Assert; /** * A {@link SecurityExpressionHandler} that uses a {@link RequestAuthorizationContext} to @@ -36,13 +39,10 @@ * @author Evgeniy Cheban * @since 5.8 */ +@NullMarked public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpressionHandler implements SecurityExpressionHandler { - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - - private String defaultRolePrefix = "ROLE_"; - @Override public EvaluationContext createEvaluationContext(Supplier authentication, RequestAuthorizationContext context) { @@ -61,11 +61,9 @@ protected SecurityExpressionOperations createSecurityExpressionRoot(Authenticati private WebSecurityExpressionRoot createSecurityExpressionRoot(Supplier authentication, RequestAuthorizationContext context) { - WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context.getRequest()); - root.setRoleHierarchy(getRoleHierarchy()); + WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context); + root.setAuthorizationManagerFactory(getAuthorizationManagerFactory()); root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setDefaultRolePrefix(this.defaultRolePrefix); return root; } @@ -73,10 +71,12 @@ private WebSecurityExpressionRoot createSecurityExpressionRoot(Supplier implements SecurityExpressionHandler { - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - - private String defaultRolePrefix = "ROLE_"; - @Override protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) { - WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, fi); + FilterInvocationExpressionRoot root = new FilterInvocationExpressionRoot(() -> authentication, fi); + root.setAuthorizationManagerFactory(getAuthorizationManagerFactory()); root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(getRoleHierarchy()); - root.setDefaultRolePrefix(this.defaultRolePrefix); return root; } @@ -53,10 +51,12 @@ protected SecurityExpressionOperations createSecurityExpressionRoot(Authenticati * {@link AuthenticationTrustResolverImpl}. * @param trustResolver the {@link AuthenticationTrustResolver} to use. Cannot be * null. + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ + @Deprecated(since = "7.0") public void setTrustResolver(AuthenticationTrustResolver trustResolver) { - Assert.notNull(trustResolver, "trustResolver cannot be null"); - this.trustResolver = trustResolver; + getDefaultAuthorizationManagerFactory().setTrustResolver(trustResolver); } /** @@ -73,9 +73,15 @@ public void setTrustResolver(AuthenticationTrustResolver trustResolver) { * If null or empty, then no default role prefix is used. *

* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_". + * @deprecated Use + * {@link #setAuthorizationManagerFactory(AuthorizationManagerFactory)} instead */ - public void setDefaultRolePrefix(String defaultRolePrefix) { - this.defaultRolePrefix = defaultRolePrefix; + @Deprecated(since = "7.0") + public void setDefaultRolePrefix(@Nullable String defaultRolePrefix) { + if (defaultRolePrefix == null) { + defaultRolePrefix = ""; + } + getDefaultAuthorizationManagerFactory().setRolePrefix(defaultRolePrefix); } } diff --git a/web/src/main/java/org/springframework/security/web/access/expression/FilterInvocationExpressionRoot.java b/web/src/main/java/org/springframework/security/web/access/expression/FilterInvocationExpressionRoot.java new file mode 100644 index 0000000000..0707eaace7 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/expression/FilterInvocationExpressionRoot.java @@ -0,0 +1,62 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access.expression; + +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.util.matcher.IpAddressMatcher; + +/** + * @author Steve Riesenberg + * @since 7.0 + */ +final class FilterInvocationExpressionRoot extends SecurityExpressionRoot { + + /** + * Allows direct access to the request object + */ + public final HttpServletRequest request; + + /** + * Creates an instance for the given {@link Supplier} of the {@link Authentication} + * and {@link HttpServletRequest}. + * @param authentication the {@link Supplier} of the {@link Authentication} to use + * @param fi the {@link FilterInvocation} to use + */ + public FilterInvocationExpressionRoot(Supplier authentication, FilterInvocation fi) { + super(authentication, fi); + this.request = fi.getRequest(); + } + + /** + * Takes a specific IP address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or + * 202.24.0.0/14). + * @param ipAddress the address or range of addresses from which the request must + * come. + * @return true if the IP address of the current request is in the required range. + */ + public boolean hasIpAddress(String ipAddress) { + IpAddressMatcher matcher = new IpAddressMatcher(ipAddress); + return matcher.matches(this.request); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java b/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java index c606887ca6..87b5692a8a 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/WebSecurityExpressionRoot.java @@ -23,6 +23,7 @@ import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.core.Authentication; import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.util.matcher.IpAddressMatcher; /** @@ -30,15 +31,20 @@ * @author Evgeniy Cheban * @since 3.0 */ -public class WebSecurityExpressionRoot extends SecurityExpressionRoot { +public class WebSecurityExpressionRoot extends SecurityExpressionRoot { /** * Allows direct access to the request object */ public final HttpServletRequest request; + /** + * @deprecated Use + * {@link #WebSecurityExpressionRoot(Supplier, RequestAuthorizationContext)} instead + */ + @Deprecated(since = "7.0") public WebSecurityExpressionRoot(Authentication a, FilterInvocation fi) { - this(() -> a, fi.getRequest()); + this(() -> a, new RequestAuthorizationContext(fi.getRequest())); } /** @@ -47,12 +53,27 @@ public WebSecurityExpressionRoot(Authentication a, FilterInvocation fi) { * @param authentication the {@link Supplier} of the {@link Authentication} to use * @param request the {@link HttpServletRequest} to use * @since 5.8 + * @deprecated Use + * {@link #WebSecurityExpressionRoot(Supplier, RequestAuthorizationContext)} instead */ + @Deprecated(since = "7.0") public WebSecurityExpressionRoot(Supplier authentication, HttpServletRequest request) { - super(authentication); + super(authentication, new RequestAuthorizationContext(request)); this.request = request; } + /** + * Creates an instance for the given {@link Supplier} of the {@link Authentication} + * and {@link HttpServletRequest}. + * @param authentication the {@link Supplier} of the {@link Authentication} to use + * @param context the {@link RequestAuthorizationContext} to use + * @since 7.0 + */ + public WebSecurityExpressionRoot(Supplier authentication, RequestAuthorizationContext context) { + super(authentication, context); + this.request = context.getRequest(); + } + /** * Takes a specific IP address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or * 202.24.0.0/14).