From 4ca53468714159fddbc9835b1e129c5c7ee1a1e4 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 8 Apr 2022 10:08:14 -0600 Subject: [PATCH] Pick up AuthorizationManager Bean Closes gh-11067 Closes gh-11068 --- .../AuthorizeHttpRequestsConfigurer.java | 16 +++- .../config/web/server/ServerHttpSecurity.java | 25 ++++++- .../AuthorizeHttpRequestsConfigurerTests.java | 36 +++++++++ .../server/AuthorizeExchangeSpecTests.java | 73 ++++++++++++++++++- 4 files changed, 145 insertions(+), 5 deletions(-) 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 184ff7e104..3c3476659d 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 @@ -21,6 +21,8 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpMethod; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; @@ -52,6 +54,10 @@ public final class AuthorizeHttpRequestsConfigurer permitAllAuthorizationManager = (a, o) -> new AuthorizationDecision(true); + static final ResolvableType REQUEST_AUTHORIZATION_MANAGER_TYPE = ResolvableType + .forType(new ParameterizedTypeReference>() { + }); + private final AuthorizationManagerRequestMatcherRegistry registry; private final AuthorizationEventPublisher publisher; @@ -137,9 +143,15 @@ public final class AuthorizeHttpRequestsConfigurer "An incomplete mapping was found for " + this.unmappedMatchers + ". Try completing it with something like requestUrls()..hasRole('USER')"); - Assert.state(this.mappingCount > 0, + if (this.mappingCount > 0) { + return postProcess(this.managerBuilder.build()); + } + if (this.getApplicationContext().getBeanNamesForType(REQUEST_AUTHORIZATION_MANAGER_TYPE).length > 0) { + return (AuthorizationManager) this.getApplicationContext() + .getBeanProvider(REQUEST_AUTHORIZATION_MANAGER_TYPE).getObject(); + } + throw new IllegalStateException( "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); - return postProcess(this.managerBuilder.build()); } @Override diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 5c0094f079..096b705374 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -37,6 +37,7 @@ import reactor.util.context.Context; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.converter.Converter; @@ -255,6 +256,10 @@ import org.springframework.web.server.WebFilterChain; */ public class ServerHttpSecurity { + private static final ResolvableType REQUEST_AUTHORIZATION_MANAGER_TYPE = ResolvableType + .forType(new ParameterizedTypeReference>() { + }); + private ServerWebExchangeMatcher securityMatcher = ServerWebExchangeMatchers.anyExchange(); private AuthorizeExchangeSpec authorizeExchange; @@ -1584,6 +1589,8 @@ public class ServerHttpSecurity { private boolean anyExchangeRegistered; + private boolean mappingRegistered; + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -1616,10 +1623,23 @@ public class ServerHttpSecurity { protected void configure(ServerHttpSecurity http) { Assert.state(this.matcher == null, () -> "The matcher " + this.matcher + " does not have an access rule defined"); - AuthorizationWebFilter result = new AuthorizationWebFilter(this.managerBldr.build()); + AuthorizationWebFilter result = new AuthorizationWebFilter(authorizationManager()); http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION); } + private ReactiveAuthorizationManager authorizationManager() { + if (this.mappingRegistered) { + return this.managerBldr.build(); + } + ReactiveAuthorizationManager anyExchange = getBeanOrNull( + REQUEST_AUTHORIZATION_MANAGER_TYPE); + if (anyExchange != null) { + return anyExchange; + } + throw new IllegalStateException( + "At least one mapping is required (for example, authorizeExchange().anyExchange().authenticated())"); + } + /** * Configures the access for a particular set of exchanges. */ @@ -1710,6 +1730,7 @@ public class ServerHttpSecurity { AuthorizeExchangeSpec.this.managerBldr .add(new ServerWebExchangeMatcherEntry<>(AuthorizeExchangeSpec.this.matcher, manager)); AuthorizeExchangeSpec.this.matcher = null; + AuthorizeExchangeSpec.this.mappingRegistered = true; return AuthorizeExchangeSpec.this; } 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 9987a79b0b..85e47dce71 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 @@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; +import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,10 +49,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; 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.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -395,6 +398,20 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithUser).andExpect(status().isOk()); } + @Test + public void getWhenOnlyAuthorizationManagerBeanThenRespondsWithOk() throws Exception { + this.spring.register(NoRequestsConfig.class, AuthorizationManagerConfig.class, BasicController.class) + .autowire(); + AuthorizationManager request = (AuthorizationManager) this.spring + .getContext().getBean("request"); + given(request.check(any(), any())).willReturn(new AuthorizationDecision(true)); + this.mvc.perform(get("/")).andExpect(status().isOk()); + verify(request).check(any(), any()); + AuthorizationManager method = (AuthorizationManager) this.spring + .getContext().getBean("method"); + verifyNoInteractions(method); + } + @EnableWebSecurity static class NoRequestsConfig { @@ -725,6 +742,25 @@ public class AuthorizeHttpRequestsConfigurerTests { } + @Configuration + static class AuthorizationManagerConfig { + + private final AuthorizationManager request = mock(AuthorizationManager.class); + + private final AuthorizationManager method = mock(AuthorizationManager.class); + + @Bean + AuthorizationManager request() { + return this.request; + } + + @Bean + AuthorizationManager method() { + return this.method; + } + + } + @RestController static class BasicController { diff --git a/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java index f47a095f09..cbeebb6faa 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -16,14 +16,29 @@ package org.springframework.security.config.web.server; +import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; +import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.security.config.Customizer.withDefaults; /** * @author Rob Winch @@ -33,6 +48,8 @@ public class AuthorizeExchangeSpecTests { ServerHttpSecurity http = ServerHttpSecurityConfigurationBuilder.httpWithDefaultAuthentication(); + public final SpringTestContext spring = new SpringTestContext(this); + @Test public void antMatchersWhenMethodAndPatternsThenDiscriminatesByMethod() { this.http.csrf().disable().authorizeExchange().pathMatchers(HttpMethod.POST, "/a", "/b").denyAll().anyExchange() @@ -107,6 +124,26 @@ public class AuthorizeExchangeSpecTests { // @formatter:on } + @Test + public void buildWhenAuthorizationManagerThenWorks() { + this.spring.register(NoRequestsConfig.class, AuthorizationManagerConfig.class).autowire(); + ReactiveAuthorizationManager request = (ReactiveAuthorizationManager) this.spring + .getContext().getBean("request"); + given(request.verify(any(), any())).willReturn(Mono.empty()); + SecurityWebFilterChain filterChain = this.spring.getContext().getBean(SecurityWebFilterChain.class); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(filterChain).build(); + // @formatter:off + client.get() + .uri("/a") + .exchange() + .expectStatus().isOk(); + // @formatter:on + verify(request).verify(any(), any()); + ReactiveAuthorizationManager method = (ReactiveAuthorizationManager) this.spring + .getContext().getBean("method"); + verifyNoInteractions(method); + } + @Test public void antMatchersWhenNoAccessAndAnotherMatcherThenThrowsException() { this.http.authorizeExchange().pathMatchers("/incomplete"); @@ -141,4 +178,38 @@ public class AuthorizeExchangeSpecTests { return WebTestClientBuilder.bindToWebFilters(this.http.build()).build(); } + @EnableWebFluxSecurity + static class NoRequestsConfig { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + return http + .authorizeExchange(withDefaults()) + .build(); + // @formatter:on + } + + } + + @Configuration + static class AuthorizationManagerConfig { + + private final ReactiveAuthorizationManager request = mock( + ReactiveAuthorizationManager.class); + + private final ReactiveAuthorizationManager method = mock(ReactiveAuthorizationManager.class); + + @Bean + ReactiveAuthorizationManager request() { + return this.request; + } + + @Bean + ReactiveAuthorizationManager method() { + return this.method; + } + + } + }