mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-25 13:32:30 +00:00
Add Support for Authorizing Spring MVC Return Types
Closes gh-16059
This commit is contained in:
parent
6438603cb6
commit
09ba5397fb
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.config.annotation.method.configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.View;
|
||||
|
||||
@Configuration
|
||||
class AuthorizationProxyWebConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
AuthorizationAdvisorProxyFactory.TargetVisitor webTargetVisitor() {
|
||||
return new WebTargetVisitor();
|
||||
}
|
||||
|
||||
static class WebTargetVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor {
|
||||
|
||||
@Override
|
||||
public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) {
|
||||
if (target instanceof ResponseEntity<?> entity) {
|
||||
return new ResponseEntity<>(proxyFactory.proxy(entity.getBody()), entity.getHeaders(),
|
||||
entity.getStatusCode());
|
||||
}
|
||||
if (target instanceof HttpEntity<?> entity) {
|
||||
return new HttpEntity<>(proxyFactory.proxy(entity.getBody()), entity.getHeaders());
|
||||
}
|
||||
if (target instanceof ModelAndView mav) {
|
||||
View view = mav.getView();
|
||||
String viewName = mav.getViewName();
|
||||
Map<String, Object> model = (Map<String, Object>) proxyFactory.proxy(mav.getModel());
|
||||
ModelAndView proxied = (view != null) ? new ModelAndView(view, model)
|
||||
: new ModelAndView(viewName, model);
|
||||
proxied.setStatus(mav.getStatus());
|
||||
return proxied;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@ -41,6 +41,9 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
private static final boolean isDataPresent = ClassUtils
|
||||
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
|
||||
|
||||
private static final boolean isWebPresent = ClassUtils
|
||||
.isPresent("org.springframework.web.servlet.DispatcherServlet", null);
|
||||
|
||||
private static final boolean isObservabilityPresent = ClassUtils
|
||||
.isPresent("io.micrometer.observation.ObservationRegistry", null);
|
||||
|
||||
@ -67,6 +70,9 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
if (isDataPresent) {
|
||||
imports.add(AuthorizationProxyDataConfiguration.class.getName());
|
||||
}
|
||||
if (isWebPresent) {
|
||||
imports.add(AuthorizationProxyWebConfiguration.class.getName());
|
||||
}
|
||||
if (isObservabilityPresent) {
|
||||
imports.add(MethodObservationConfiguration.class.getName());
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@ -38,6 +38,9 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
|
||||
private static final boolean isDataPresent = ClassUtils
|
||||
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
|
||||
|
||||
private static final boolean isWebPresent = ClassUtils.isPresent("org.springframework.web.server.ServerWebExchange",
|
||||
null);
|
||||
|
||||
private static final boolean isObservabilityPresent = ClassUtils
|
||||
.isPresent("io.micrometer.observation.ObservationRegistry", null);
|
||||
|
||||
@ -61,6 +64,9 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
|
||||
if (isDataPresent) {
|
||||
imports.add(AuthorizationProxyDataConfiguration.class.getName());
|
||||
}
|
||||
if (isWebPresent) {
|
||||
imports.add(AuthorizationProxyWebConfiguration.class.getName());
|
||||
}
|
||||
if (isObservabilityPresent) {
|
||||
imports.add(ReactiveMethodObservationConfiguration.class.getName());
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ import org.springframework.context.annotation.Role;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.annotation.BusinessService;
|
||||
@ -90,7 +92,6 @@ import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
|
||||
import org.springframework.security.authorization.method.MethodInvocationResult;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.config.observation.SecurityObservationSettings;
|
||||
@ -109,6 +110,7 @@ import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@ -729,6 +731,49 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedResponseEntityThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.webFindById("1").getBody();
|
||||
assertThatNoException().isThrownBy(flight::getAltitude);
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThat(flights.webFindById("5").getBody()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedResponseEntityThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.webFindById("1").getBody();
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedModelAndViewThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = (Flight) flights.webViewFindById("1").getModel().get("flight");
|
||||
assertThatNoException().isThrownBy(flight::getAltitude);
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThat(flights.webViewFindById("5").getModel().get("flight")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedModelAndViewThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = (Flight) flights.webViewFindById("1").getModel().get("flight");
|
||||
assertThatNoException().isThrownBy(flight::getSeats);
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
|
||||
assertThat(flights.webViewFindById("5").getModel().get("flight")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
@ -1601,8 +1646,8 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
|
||||
static TargetVisitor skipValueTypes() {
|
||||
return TargetVisitor.defaultsSkipValueTypes();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -1646,6 +1691,22 @@ public class PrePostMethodSecurityConfigurationTests {
|
||||
this.flights.remove(id);
|
||||
}
|
||||
|
||||
ResponseEntity<Flight> webFindById(String id) {
|
||||
Flight flight = this.flights.get(id);
|
||||
if (flight == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(flight);
|
||||
}
|
||||
|
||||
ModelAndView webViewFindById(String id) {
|
||||
Flight flight = this.flights.get(id);
|
||||
if (flight == null) {
|
||||
return new ModelAndView("error", HttpStatusCode.valueOf(404));
|
||||
}
|
||||
return new ModelAndView("flights", Map.of("flight", flight));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
@ -40,6 +40,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
@ -54,9 +56,9 @@ import org.springframework.security.access.prepost.PreFilter;
|
||||
import org.springframework.security.authorization.AuthorizationDeniedException;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisor;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
|
||||
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
|
||||
import org.springframework.security.authorization.method.AuthorizeReturnObject;
|
||||
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -65,6 +67,7 @@ import org.springframework.security.test.context.annotation.SecurityTestExecutio
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@ -361,6 +364,48 @@ public class PrePostReactiveMethodSecurityConfigurationTests {
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedResponseEntityThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.webFindById("1").block().getBody();
|
||||
assertThatNoException().isThrownBy(() -> flight.getAltitude().block());
|
||||
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedResponseEntityThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = flights.webFindById("1").block().getBody();
|
||||
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "airplane:read")
|
||||
public void findByIdWhenAuthorizedModelAndViewThenAuthorizes() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = (Flight) flights.webViewFindById("1").block().getModel().get("flight");
|
||||
assertThatNoException().isThrownBy(() -> flight.getAltitude().block());
|
||||
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
|
||||
assertThat(flights.webViewFindById("5").block().getModel().get("flight")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findByIdWhenUnauthorizedModelAndViewThenDenies() {
|
||||
this.spring.register(AuthorizeResultConfig.class).autowire();
|
||||
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
|
||||
Flight flight = (Flight) flights.webViewFindById("1").block().getModel().get("flight");
|
||||
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
|
||||
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
|
||||
assertThat(flights.webViewFindById("5").block().getModel().get("flight")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "seating:read")
|
||||
public void findAllWhenUnauthorizedResultThenDenies() {
|
||||
@ -659,8 +704,8 @@ public class PrePostReactiveMethodSecurityConfigurationTests {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
|
||||
return (f) -> f.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes());
|
||||
static TargetVisitor skipValueTypes() {
|
||||
return TargetVisitor.defaultsSkipValueTypes();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -724,6 +769,22 @@ public class PrePostReactiveMethodSecurityConfigurationTests {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
Mono<ResponseEntity<Flight>> webFindById(String id) {
|
||||
Flight flight = this.flights.get(id);
|
||||
if (flight == null) {
|
||||
return Mono.just(ResponseEntity.notFound().build());
|
||||
}
|
||||
return Mono.just(ResponseEntity.ok(flight));
|
||||
}
|
||||
|
||||
Mono<ModelAndView> webViewFindById(String id) {
|
||||
Flight flight = this.flights.get(id);
|
||||
if (flight == null) {
|
||||
return Mono.just(new ModelAndView("error", HttpStatusCode.valueOf(404)));
|
||||
}
|
||||
return Mono.just(new ModelAndView("flights", Map.of("flight", flight)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AuthorizeReturnObject
|
||||
|
Loading…
x
Reference in New Issue
Block a user