Skip to content

Commit

Permalink
Add AuthorizationResult Auto-Proxy Support
Browse files Browse the repository at this point in the history
  • Loading branch information
jzheaux committed Mar 1, 2024
1 parent 6d290c9 commit 2f3457e
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.config.annotation.method.configuration;

import java.util.ArrayList;
import java.util.List;

import org.aopalliance.intercept.MethodInterceptor;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
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.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationProxyMethodInterceptor;
import org.springframework.security.authorization.object.AuthorizationProxyFactory;

@Configuration(proxyBeanMethods = false)
public class AuthorizationProxyConfiguration implements AopInfrastructureBean {

@Bean
static AuthorizationProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider) {
List<Advisor> advisors = new ArrayList<>();
provider.forEach(advisors::add);
AnnotationAwareOrderComparator.sort(advisors);
return new AuthorizationProxyFactory(advisors);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor authorizationProxyMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
return new AuthorizationProxyMethodInterceptor(authorizationProxyFactory);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -48,7 +49,7 @@
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class Jsr250MethodSecurityConfiguration implements ImportAware {
final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {

private int interceptorOrderOffset;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
registerAsAdvisor("postAuthorizeAuthorization", registry);
registerAsAdvisor("securedAuthorization", registry);
registerAsAdvisor("jsr250Authorization", registry);
registerAsAdvisor("authorizationProxy", registry);
}

private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
if (annotation.jsr250Enabled()) {
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}
imports.add(AuthorizationProxyConfiguration.class.getName());
return imports.toArray(new String[0]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.jetbrains.annotations.Nullable;

import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
Expand All @@ -36,14 +35,14 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationAdvisor;
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
Expand All @@ -65,7 +64,7 @@
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class PrePostMethodSecurityConfiguration implements ImportAware {
final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {

private int interceptorOrderOffset;

Expand Down Expand Up @@ -175,8 +174,8 @@ public void setImportMetadata(AnnotationMetadata importMetadata) {
this.interceptorOrderOffset = annotation.offset();
}

private static final class DeferringMethodInterceptor<M extends Ordered & MethodInterceptor & PointcutAdvisor>
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
implements AuthorizationAdvisor {

private final Pointcut pointcut;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -48,7 +49,7 @@
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
final class SecuredMethodSecurityConfiguration implements ImportAware {
final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {

private int interceptorOrderOffset;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;

Expand Down Expand Up @@ -60,6 +63,7 @@
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeResult;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
Expand All @@ -80,6 +84,7 @@

import static org.assertj.core.api.Assertions.assertThat;
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.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -662,6 +667,44 @@ public void methodWhenPostFilterMetaAnnotationThenFilters() {
.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");
}

private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
Expand Down Expand Up @@ -1061,4 +1104,76 @@ List<String> resultsContainDave(List<String> list) {

}

@EnableMethodSecurity
static class AuthorizeResultConfig {

@Bean
FlightRepository flights() {
FlightRepository flights = new FlightRepository();
flights.save(new Flight("1", 35000d, 35));
flights.save(new Flight("2", 32000d, 72));
return flights;
}

@Bean
RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
}

}

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

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;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 org.aopalliance.intercept.MethodInterceptor;

import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;

public interface AuthorizationAdvisor extends Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public enum AuthorizationInterceptorsOrder {
*/
POST_FILTER,

SECURE_RESULT,

LAST(Integer.MAX_VALUE);

private static final int INTERVAL = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
Expand All @@ -48,8 +45,7 @@
* @author Josh Cummings
* @since 5.6
*/
public final class AuthorizationManagerAfterMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class AuthorizationManagerAfterMethodInterceptor implements AuthorizationAdvisor {

private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.annotation.Secured;
Expand All @@ -52,8 +49,7 @@
* @author Josh Cummings
* @since 5.6
*/
public final class AuthorizationManagerBeforeMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public final class AuthorizationManagerBeforeMethodInterceptor implements AuthorizationAdvisor {

private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;

Expand Down
Loading

0 comments on commit 2f3457e

Please sign in to comment.