Add AuthorizeReturnObject

Closes gh-14597
This commit is contained in:
Josh Cummings 2024-03-15 13:11:26 -06:00
parent 778935d5b3
commit d169d5a835
19 changed files with 778 additions and 12 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -103,6 +103,13 @@ public class PostAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
} }
@Test
public void nestedDenyAllPostAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
interface SecuredInterface { interface SecuredInterface {
@PostAuthorize("hasRole('X')") @PostAuthorize("hasRole('X')")
@ -134,6 +141,10 @@ public class PostAuthorizeAspectTests {
privateMethod(); privateMethod();
} }
NestedObject myObject() {
return new NestedObject();
}
} }
static class SecuredImplSubclass extends SecuredImpl { static class SecuredImplSubclass extends SecuredImpl {
@ -157,4 +168,13 @@ public class PostAuthorizeAspectTests {
} }
static class NestedObject {
@PostAuthorize("denyAll")
void denyAllMethod() {
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,6 +54,11 @@ public class PostFilterAspectTests {
assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine"); assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
} }
@Test
public void nestedDenyAllPostFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod()).isEmpty();
}
static class PrePostSecured { static class PrePostSecured {
@PostFilter("filterObject.startsWith('a')") @PostFilter("filterObject.startsWith('a')")
@ -61,6 +66,19 @@ public class PostFilterAspectTests {
return objects; return objects;
} }
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PostFilter("filterObject == null")
List<String> denyAllMethod() {
return new ArrayList<>(List.of("deny"));
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -103,6 +103,13 @@ public class PreAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
} }
@Test
public void nestedDenyAllPreAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
interface SecuredInterface { interface SecuredInterface {
@PreAuthorize("hasRole('X')") @PreAuthorize("hasRole('X')")
@ -134,6 +141,10 @@ public class PreAuthorizeAspectTests {
privateMethod(); privateMethod();
} }
NestedObject myObject() {
return new NestedObject();
}
} }
static class SecuredImplSubclass extends SecuredImpl { static class SecuredImplSubclass extends SecuredImpl {
@ -157,4 +168,13 @@ public class PreAuthorizeAspectTests {
} }
static class NestedObject {
@PreAuthorize("denyAll")
void denyAllMethod() {
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,6 +54,11 @@ public class PreFilterAspectTests {
assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine"); assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
} }
@Test
public void nestedDenyAllPreFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod(new ArrayList<>(List.of("deny")))).isEmpty();
}
static class PrePostSecured { static class PrePostSecured {
@PreFilter("filterObject.startsWith('a')") @PreFilter("filterObject.startsWith('a')")
@ -61,6 +66,19 @@ public class PreFilterAspectTests {
return objects; return objects;
} }
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PreFilter("filterObject == null")
List<String> denyAllMethod(List<String> list) {
return list;
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@ -27,6 +29,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
final class AuthorizationProxyConfiguration implements AopInfrastructureBean { final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
@ -41,4 +44,17 @@ final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
return factory; return factory;
} }
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
authorizationProxyFactory);
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
advisors.add(interceptor);
authorizationProxyFactory.setAdvisors(advisors);
return interceptor;
}
} }

View File

@ -33,6 +33,7 @@ class MethodSecurityAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
registerAsAdvisor("postAuthorizeAuthorization", registry); registerAsAdvisor("postAuthorizeAuthorization", registry);
registerAsAdvisor("securedAuthorization", registry); registerAsAdvisor("securedAuthorization", registry);
registerAsAdvisor("jsr250Authorization", registry); registerAsAdvisor("jsr250Authorization", registry);
registerAsAdvisor("authorizeReturnObject", registry);
} }
private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) { private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {

View File

@ -19,6 +19,8 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@ -27,6 +29,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role; import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean { final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
@ -42,4 +45,17 @@ final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructure
return factory; return factory;
} }
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory) {
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
authorizationProxyFactory);
List<AuthorizationAdvisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
advisors.add(interceptor);
authorizationProxyFactory.setAdvisors(advisors);
return interceptor;
}
} }

View File

@ -21,7 +21,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -60,6 +63,7 @@ import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
@ -80,6 +84,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -662,6 +667,79 @@ public class PrePostMethodSecurityConfigurationTests {
.containsExactly("dave"); .containsExactly("dave");
} }
@Test
@WithMockUser(authorities = "airplane:read")
public void findByIdWhenAuthorizedResultThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Flight flight = flights.findById("1");
assertThatNoException().isThrownBy(flight::getAltitude);
assertThatNoException().isThrownBy(flight::getSeats);
}
@Test
@WithMockUser(authorities = "seating:read")
public void findByIdWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
Flight flight = flights.findById("1");
assertThatNoException().isThrownBy(flight::getSeats);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
}
@Test
@WithMockUser(authorities = "seating:read")
public void findAllWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
assertThatNoException().isThrownBy(flight::getSeats);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
});
}
@Test
public void removeWhenAuthorizedResultThenRemoves() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.remove("1");
}
@Test
@WithMockUser(authorities = "airplane:read")
public void findAllWhenPostFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll()
.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
.doesNotContain("Kevin Mitnick"));
}
@Test
@WithMockUser(authorities = "airplane:read")
public void findAllWhenPreFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
flight.board(new ArrayList<>(List.of("John")));
assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
flight.board(new ArrayList<>(List.of("John Doe")));
assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
});
}
@Test
@WithMockUser(authorities = "seating:read")
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
flights.findAll().forEachRemaining((flight) -> {
List<Passenger> passengers = flight.getPassengers();
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(passenger::getName));
});
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() { private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
} }
@ -1061,4 +1139,113 @@ public class PrePostMethodSecurityConfigurationTests {
} }
@EnableMethodSecurity
@Configuration
static class AuthorizeResultConfig {
@Bean
FlightRepository flights() {
FlightRepository flights = new FlightRepository();
Flight one = new Flight("1", 35000d, 35);
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
flights.save(one);
Flight two = new Flight("2", 32000d, 72);
two.board(new ArrayList<>(List.of("Albert Einstein")));
flights.save(two);
return flights;
}
@Bean
RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
}
}
@AuthorizeReturnObject
static class FlightRepository {
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
Iterator<Flight> findAll() {
return this.flights.values().iterator();
}
Flight findById(String id) {
return this.flights.get(id);
}
Flight save(Flight flight) {
this.flights.put(flight.getId(), flight);
return flight;
}
void remove(String id) {
this.flights.remove(id);
}
}
static class Flight {
private final String id;
private final Double altitude;
private final Integer seats;
private final List<Passenger> passengers = new ArrayList<>();
Flight(String id, Double altitude, Integer seats) {
this.id = id;
this.altitude = altitude;
this.seats = seats;
}
String getId() {
return this.id;
}
@PreAuthorize("hasAuthority('airplane:read')")
Double getAltitude() {
return this.altitude;
}
@PreAuthorize("hasAuthority('seating:read')")
Integer getSeats() {
return this.seats;
}
@AuthorizeReturnObject
@PostAuthorize("hasAuthority('seating:read')")
@PostFilter("filterObject.name != 'Kevin Mitnick'")
List<Passenger> getPassengers() {
return this.passengers;
}
@PreAuthorize("hasAuthority('seating:read')")
@PreFilter("filterObject.contains(' ')")
void board(List<String> passengers) {
for (String passenger : passengers) {
this.passengers.add(new Passenger(passenger));
}
}
}
public static class Passenger {
String name;
public Passenger(String name) {
this.name = name;
}
@PreAuthorize("hasAuthority('airplane:read')")
public String getName() {
return this.name;
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,20 +16,36 @@
package org.springframework.security.config.annotation.method.configuration; package org.springframework.security.config.annotation.method.configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.intercept.method.MockMethodInvocation; import org.springframework.security.access.intercept.method.MockMethodInvocation;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -85,6 +101,112 @@ public class ReactiveMethodSecurityConfigurationTests {
assertThat(root.hasRole("ABC")).isTrue(); assertThat(root.hasRole("ABC")).isTrue();
} }
@Test
public void findByIdWhenAuthorizedResultThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
}
@Test
public void findByIdWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(1)
.verifyComplete();
StepVerifier
.create(flights.findById("1")
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Test
public void findAllWhenUnauthorizedResultThenDenies() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getSeats)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNextCount(2)
.verifyComplete();
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getAltitude)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Test
public void removeWhenAuthorizedResultThenRemoves() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
StepVerifier.create(flights.remove("1").contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyComplete();
}
@Test
public void findAllWhenPostFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNext("Marie Curie", "Ada Lovelace", "Albert Einstein")
.verifyComplete();
}
@Test
public void findAllWhenPreFilterThenFilters() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "airplane:read");
StepVerifier
.create(flights.findAll()
.flatMap((flight) -> flight.board(Flux.just("John Doe", "John")).then(Mono.just(flight)))
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.expectNext("Marie Curie", "Ada Lovelace", "John Doe", "Albert Einstein", "John Doe")
.verifyComplete();
}
@Test
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
this.spring.register(AuthorizeResultConfig.class).autowire();
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
TestingAuthenticationToken pilot = new TestingAuthenticationToken("user", "pass", "seating:read");
StepVerifier
.create(flights.findAll()
.flatMap(Flight::getPassengers)
.flatMap(Passenger::getName)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(pilot)))
.verifyError(AccessDeniedException.class);
}
@Configuration @Configuration
@EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration @EnableReactiveMethodSecurity // this imports ReactiveMethodSecurityConfiguration
static class WithRolePrefixConfiguration { static class WithRolePrefixConfiguration {
@ -108,4 +230,112 @@ public class ReactiveMethodSecurityConfigurationTests {
} }
@EnableReactiveMethodSecurity
@Configuration
static class AuthorizeResultConfig {
@Bean
FlightRepository flights() {
FlightRepository flights = new FlightRepository();
Flight one = new Flight("1", 35000d, 35);
one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
flights.save(one).block();
Flight two = new Flight("2", 32000d, 72);
two.board(Flux.just("Albert Einstein")).block();
flights.save(two).block();
return flights;
}
@Bean
Function<Passenger, Mono<Boolean>> isNotKevin() {
return (passenger) -> passenger.getName().map((name) -> !name.equals("Kevin Mitnick"));
}
}
@AuthorizeReturnObject
static class FlightRepository {
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
Flux<Flight> findAll() {
return Flux.fromIterable(this.flights.values());
}
Mono<Flight> findById(String id) {
return Mono.just(this.flights.get(id));
}
Mono<Flight> save(Flight flight) {
this.flights.put(flight.getId(), flight);
return Mono.just(flight);
}
Mono<Void> remove(String id) {
this.flights.remove(id);
return Mono.empty();
}
}
static class Flight {
private final String id;
private final Double altitude;
private final Integer seats;
private final List<Passenger> passengers = new ArrayList<>();
Flight(String id, Double altitude, Integer seats) {
this.id = id;
this.altitude = altitude;
this.seats = seats;
}
String getId() {
return this.id;
}
@PreAuthorize("hasAuthority('airplane:read')")
Mono<Double> getAltitude() {
return Mono.just(this.altitude);
}
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
Mono<Integer> getSeats() {
return Mono.just(this.seats);
}
@AuthorizeReturnObject
@PostAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
@PostFilter("@isNotKevin.apply(filterObject)")
Flux<Passenger> getPassengers() {
return Flux.fromIterable(this.passengers);
}
@PreAuthorize("hasAnyAuthority('seating:read', 'airplane:read')")
@PreFilter("filterObject.contains(' ')")
Mono<Void> board(Flux<String> passengers) {
return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then();
}
}
public static class Passenger {
String name;
public Passenger(String name) {
this.name = name;
}
@PreAuthorize("hasAuthority('airplane:read')")
public Mono<String> getName() {
return Mono.just(this.name);
}
}
} }

View File

@ -42,6 +42,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -83,6 +84,7 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx
advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize()); advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize());
advisors.add(new PreFilterAuthorizationMethodInterceptor()); advisors.add(new PreFilterAuthorizationMethodInterceptor());
advisors.add(new PostFilterAuthorizationMethodInterceptor()); advisors.add(new PostFilterAuthorizationMethodInterceptor());
advisors.add(new AuthorizeReturnObjectMethodInterceptor(this));
setAdvisors(advisors); setAdvisors(advisors);
} }

View File

@ -32,6 +32,7 @@ import org.springframework.aop.framework.ProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
@ -72,6 +73,7 @@ public final class ReactiveAuthorizationAdvisorProxyFactory implements Authoriza
advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize()); advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor()); advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor()); advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
advisors.add(new AuthorizeReturnObjectMethodInterceptor(this));
this.defaults.setAdvisors(advisors); this.defaults.setAdvisors(advisors);
} }

View File

@ -43,12 +43,14 @@ public enum AuthorizationInterceptorsOrder {
JSR250, JSR250,
POST_AUTHORIZE, SECURE_RESULT(450),
POST_AUTHORIZE(500),
/** /**
* {@link PostFilterAuthorizationMethodInterceptor} * {@link PostFilterAuthorizationMethodInterceptor}
*/ */
POST_FILTER, POST_FILTER(600),
LAST(Integer.MAX_VALUE); LAST(Integer.MAX_VALUE);

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2024 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.method;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Wraps Spring Security method authorization advice around the return object of any
* method this annotation is applied to.
*
* <p>
* Placing this at the class level is semantically identical to placing it on each method
* in that class.
* </p>
*
* @author Josh Cummings
* @since 6.3
* @see AuthorizeReturnObjectMethodInterceptor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface AuthorizeReturnObject {
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2002-2024 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.method;
import java.lang.reflect.Method;
import java.util.function.Predicate;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.Pointcuts;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.security.authorization.AuthorizationProxyFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A method interceptor that applies the given {@link AuthorizationProxyFactory} to any
* return value annotated with {@link AuthorizeReturnObject}
*
* @author Josh Cummings
* @since 6.3
* @see org.springframework.security.authorization.AuthorizationAdvisorProxyFactory
*/
public final class AuthorizeReturnObjectMethodInterceptor implements AuthorizationAdvisor {
private final AuthorizationProxyFactory authorizationProxyFactory;
private Pointcut pointcut = Pointcuts.intersection(
new MethodReturnTypePointcut(Predicate.not(ClassUtils::isVoidType)),
AuthorizationMethodPointcuts.forAnnotations(AuthorizeReturnObject.class));
private int order = AuthorizationInterceptorsOrder.SECURE_RESULT.getOrder();
public AuthorizeReturnObjectMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
Assert.notNull(authorizationProxyFactory, "authorizationManager cannot be null");
this.authorizationProxyFactory = authorizationProxyFactory;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object result = mi.proceed();
if (result == null) {
return null;
}
return this.authorizationProxyFactory.proxy(result);
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@inheritDoc}
*/
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
public void setPointcut(Pointcut pointcut) {
this.pointcut = pointcut;
}
@Override
public Advice getAdvice() {
return this;
}
@Override
public boolean isPerInstance() {
return true;
}
static final class MethodReturnTypePointcut extends StaticMethodMatcherPointcut {
private final Predicate<Class<?>> returnTypeMatches;
MethodReturnTypePointcut(Predicate<Class<?>> returnTypeMatches) {
this.returnTypeMatches = returnTypeMatches;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return this.returnTypeMatches.test(method.getReturnType());
}
}
}

View File

@ -284,7 +284,7 @@ public class AuthorizationAdvisorProxyFactoryTests {
public void proxyWhenPreAuthorizeForClassThenHonors() { public void proxyWhenPreAuthorizeForClassThenHonors() {
AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory();
Class<Flight> clazz = proxy(factory, Flight.class); Class<Flight> clazz = proxy(factory, Flight.class);
assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$");
Flight secured = proxy(factory, this.flight); Flight secured = proxy(factory, this.flight);
assertThat(secured.getClass()).isSameAs(clazz); assertThat(secured.getClass()).isSameAs(clazz);
SecurityContextHolder.getContext().setAuthentication(this.user); SecurityContextHolder.getContext().setAuthentication(this.user);

View File

@ -117,7 +117,7 @@ public class ReactiveAuthorizationAdvisorProxyFactoryTests {
public void proxyWhenPreAuthorizeForClassThenHonors() { public void proxyWhenPreAuthorizeForClassThenHonors() {
ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
Class<Flight> clazz = proxy(factory, Flight.class); Class<Flight> clazz = proxy(factory, Flight.class);
assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$");
Flight secured = proxy(factory, this.flight); Flight secured = proxy(factory, this.flight);
StepVerifier StepVerifier
.create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user))) .create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))

View File

@ -1707,8 +1707,7 @@ For interfaces, either annotations or the `-parameters` approach must be used.
Spring Security also supports wrapping any object that is annotated its method security annotations. Spring Security also supports wrapping any object that is annotated its method security annotations.
To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured. The simplest way to achieve this is to mark any method that returns the object you wish to authorize with the `@AuthorizeReturnObject` annotation.
If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
For example, consider the following `User` class: For example, consider the following `User` class:
@ -1746,6 +1745,89 @@ class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val
---- ----
====== ======
Given an interface like this one:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
public class UserRepository {
@AuthorizeReturnObject
Optional<User> findByName(String name) {
// ...
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
class UserRepository {
@AuthorizeReturnObject
fun findByName(name:String?): Optional<User?>? {
// ...
}
}
----
======
Then any `User` that is returned from `findById` will be secured like other Spring Security-protected components:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenAuthorizes() {
Optional<User> securedUser = users.findByName("name");
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
import jdk.incubator.vector.VectorOperators.Test
import java.nio.file.AccessDeniedException
import java.util.*
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenAuthorizes() {
val securedUser: Optional<User> = users.findByName("name")
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
}
----
======
[NOTE]
====
`@AuthorizeReturnObject` can be placed at the class level. Note, though, that this means Spring Security will proxy any return object, including ``String``, ``Integer`` and other types.
This is often not what you want to do.
In most cases, you will want to annotate the individual methods.
====
=== Programmatically Proxying
You can also programmatically proxy a given object.
To achieve this, you can autowire the provided `AuthorizationProxyFactory` instance, which is based on which method security interceptors you have configured.
If you are using `@EnableMethodSecurity`, then this means that it will by default have the interceptors for `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter`.
You can proxy an instance of user in the following way: You can proxy an instance of user in the following way:
[tabs] [tabs]

View File

@ -11,6 +11,7 @@ Below are the highlights of the release.
== Authorization == Authorization
- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security - https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
- https://github.com/spring-projects/spring-security/issues/14597[gh-14597] - xref:servlet/authorization/method-security.adoc[docs] - Add Securing of Return Values
== Configuration == Configuration