Skip to content

Commit

Permalink
Added a new authorization method to AuthorizationManager that returns…
Browse files Browse the repository at this point in the history
… AuthorizationResult.

Closes spring-projectsgh-14843
  • Loading branch information
Max Batischev authored and Max Batischev committed Apr 7, 2024
2 parents 011fea2 + c8e5fbf commit 79e410c
Show file tree
Hide file tree
Showing 36 changed files with 586 additions and 204 deletions.
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -100,6 +100,10 @@ updates:
- dependency-name: org.mockito:mockito-bom
update-types:
- version-update:semver-major
- dependency-name: com.gradle.enterprise
update-types:
- version-update:semver-major
- version-update:semver-minor
- dependency-name: '*'
update-types:
- version-update:semver-major
Expand Down
Expand Up @@ -19,18 +19,29 @@
import java.util.function.Supplier;

import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;

final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
final class DeferringObservationAuthorizationManager<T>
implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {

private final Supplier<AuthorizationManager<T>> delegate;

private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();

private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();

DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
AuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
Expand All @@ -40,11 +51,28 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
}
return new ObservationAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}

@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, T object) {
return this.delegate.get().authorize(authentication, object);
}

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}

@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}

}
Expand Up @@ -19,19 +19,31 @@
import java.util.function.Supplier;

import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;

final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {

private final Supplier<ReactiveAuthorizationManager<T>> delegate;

private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();

private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();

DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
ReactiveAuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
Expand All @@ -41,11 +53,28 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
}
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}

@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return this.delegate.get().check(authentication, object);
}

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}

@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}

}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
Expand Down Expand Up @@ -42,7 +42,7 @@ protected String getBeanClassName(Element element) {
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
String dataSource = element.getAttribute(ATT_DATA_SOURCE);
if (dataSource != null) {
if (StringUtils.hasText(dataSource)) {
builder.addPropertyReference("dataSource", dataSource);
}
else {
Expand Down
Expand Up @@ -18,6 +18,8 @@

import reactor.core.publisher.Mono;

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -45,4 +47,20 @@ public boolean check(Authentication authentication, String message) {
return message != null && message.contains(authentication.getName());
}

public AuthorizationResult checkResult(boolean result) {
return new AuthzResult(result);
}

public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
return Mono.just(checkResult(result));
}

public static class AuthzResult extends AuthorizationDecision {

public AuthzResult(boolean granted) {
super(granted);
}

}

}
Expand Up @@ -173,6 +173,11 @@ public interface MethodSecurityService {
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();

@PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
@PostAuthorize(value = "@authz.checkResult(!#result)",
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
String checkCustomResult(boolean result);

class StarMaskingHandler implements MethodAuthorizationDeniedHandler {

@Override
Expand Down Expand Up @@ -300,7 +305,7 @@ public Object postProcessResult(MethodInvocationResult methodInvocationResult,
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class)
@PostAuthorize(value = "hasRole('{role}')", postProcessorClass = NullPostProcessor.class)
@interface NullDenied {

String role();
Expand Down
Expand Up @@ -28,4 +28,14 @@ MethodSecurityService service() {
return new MethodSecurityServiceImpl();
}

@Bean
ReactiveMethodSecurityService reactiveService() {
return new ReactiveMethodSecurityServiceImpl();
}

@Bean
Authz authz() {
return new Authz();
}

}
Expand Up @@ -197,4 +197,9 @@ public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}

@Override
public String checkCustomResult(boolean result) {
return "ok";
}

}
Expand Up @@ -66,6 +66,8 @@
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer;
Expand All @@ -92,6 +94,8 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;

/**
* Tests for {@link PrePostMethodSecurityConfiguration}.
Expand Down Expand Up @@ -823,7 +827,9 @@ void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHan
@Test
@WithMockUser
void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() {
this.spring.register(MethodSecurityServiceEnabledConfig.class, MethodSecurityService.NullPostProcessor.class)
this.spring
.register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class,
MethodSecurityService.NullPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String result = service.postAuthorizeDeniedWithNullDenied();
Expand Down Expand Up @@ -925,6 +931,23 @@ void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
assertThat(user.name()).isEqualTo("Protected");
}

@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false)).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true)).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}

private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
Expand Down Expand Up @@ -1248,7 +1271,7 @@ PrePostTemplateDefaults methodSecurityDefaults() {
}

@Bean
MetaAnnotationService methodSecurityService() {
MetaAnnotationService metaAnnotationService() {
return new MetaAnnotationService();
}

Expand Down Expand Up @@ -1449,4 +1472,23 @@ public String getName() {

}

@EnableMethodSecurity
static class CustomResultConfig {

MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);

MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);

@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}

@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}

}

}
Expand Up @@ -47,15 +47,23 @@
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.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;

/**
* @author Tadaya Tsuyukubo
Expand All @@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {

public final SpringTestContext spring = new SpringTestContext(this);

@Autowired
@Autowired(required = false)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;

@Test
Expand Down Expand Up @@ -212,6 +220,23 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
.verifyError(AccessDeniedException.class);
}

@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false).block()).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true).block()).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}

private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(authorities);
}
Expand Down Expand Up @@ -353,4 +378,23 @@ public Mono<String> getName() {

}

@EnableReactiveMethodSecurity
static class CustomResultConfig {

MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);

MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);

@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}

@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}

}

}

0 comments on commit 79e410c

Please sign in to comment.