Add Support for Authorizing Spring MVC Return Types

Closes gh-16059
This commit is contained in:
Josh Cummings
2025-04-10 10:42:08 -06:00
parent 6438603cb6
commit 09ba5397fb
5 changed files with 208 additions and 9 deletions

View File

@@ -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

View File

@@ -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