Skip to content

Commit

Permalink
Publish Authorization Events on WebFlux
Browse files Browse the repository at this point in the history
  • Loading branch information
marcusdacoregio committed Dec 21, 2023
1 parent c19f3d9 commit ada9549
Show file tree
Hide file tree
Showing 18 changed files with 1,169 additions and 25 deletions.
Expand Up @@ -28,6 +28,7 @@
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
Expand Down Expand Up @@ -57,10 +58,14 @@ static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider,
ObjectProvider<ReactiveAuthorizationEventPublisher> eventPublisherProvider) {
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
.preAuthorize(authorizationManager);
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
return interceptor;
}

@Bean
Expand All @@ -73,10 +78,14 @@ static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider,
ObjectProvider<ReactiveAuthorizationEventPublisher> eventPublisherProvider) {
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
.postAuthorize(authorizationManager);
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
return interceptor;
}

@Bean
Expand Down
Expand Up @@ -56,7 +56,9 @@
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.SpringReactiveAuthorizationEventPublisher;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
Expand Down Expand Up @@ -1794,6 +1796,8 @@ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegis

private PathPatternParser pathPatternParser;

private ReactiveAuthorizationEventPublisher authorizationEventPublisher;

/**
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
* @return the {@link ServerHttpSecurity} to continue configuring
Expand Down Expand Up @@ -1851,9 +1855,23 @@ protected void configure(ServerHttpSecurity http) {
manager = new ObservationReactiveAuthorizationManager<>(registry, manager);
}
AuthorizationWebFilter result = new AuthorizationWebFilter(manager);
ReactiveAuthorizationEventPublisher authorizationEventPublisher = getAuthorizationEventPublisher(http);
if (authorizationEventPublisher != null) {
result.setAuthorizationEventPublisher(authorizationEventPublisher);
}
http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION);
}

private ReactiveAuthorizationEventPublisher getAuthorizationEventPublisher(ServerHttpSecurity http) {
if (this.authorizationEventPublisher == null) {
this.authorizationEventPublisher = getBeanOrNull(ReactiveAuthorizationEventPublisher.class);
}
if (this.authorizationEventPublisher == null && http.context != null) {
this.authorizationEventPublisher = new SpringReactiveAuthorizationEventPublisher(http.context);
}
return this.authorizationEventPublisher;
}

/**
* Configures the access for a particular set of exchanges.
*/
Expand Down
@@ -0,0 +1,236 @@
/*
* Copyright 2002-2023 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.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
import org.springframework.security.authorization.event.ReactiveAuthorizationDeniedEvent;
import org.springframework.security.authorization.event.ReactiveAuthorizationEvent;
import org.springframework.security.authorization.event.ReactiveAuthorizationGrantedEvent;
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.GrantedAuthority;
import org.springframework.security.test.context.support.ReactorContextTestExecutionListener;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@TestExecutionListeners(
listeners = { WithSecurityContextTestExecutionListener.class, ReactorContextTestExecutionListener.class })
public class ReactiveAuthorizationManagerMethodSecurityConfigurationTests {

public final SpringTestContext spring = new SpringTestContext(this);

@Autowired
ReactiveMessageService messageService;

@Autowired
ReactiveAuthorizationEventPublisher eventPublisher;

@Autowired
MyEventListener eventListener;

AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();

@Test
void preAuthorizeMonoWhenDeniedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.monoPreAuthorizeHasRoleFindById(1))
.expectError(AccessDeniedException.class)
.verify();
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
}

@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeMonoWhenGrantedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.monoPreAuthorizeHasRoleFindById(1)).verifyComplete();
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
StepVerifier.create(event.getAuthentication())
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.contains("ROLE_ADMIN"))
.verifyComplete();
}

@Test
void preAuthorizeFluxWhenDeniedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.fluxPreAuthorizeHasRoleFindById(1))
.expectError(AccessDeniedException.class)
.verify();
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
}

@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeFluxWhenGrantedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.fluxPreAuthorizeHasRoleFindById(1)).verifyComplete();
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
StepVerifier.create(event.getAuthentication())
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.contains("ROLE_ADMIN"))
.verifyComplete();
}

@Test
void postAuthorizeMonoWhenDeniedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.monoPostAuthorizeFindById(1))
.expectError(AccessDeniedException.class)
.verify();
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
}

@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeMonoWhenGrantedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.monoPostAuthorizeFindById(1)).expectNext("user").verifyComplete();
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
StepVerifier.create(event.getAuthentication())
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.contains("ROLE_ADMIN"))
.verifyComplete();
}

@Test
@WithMockUser(username = "notuser")
void postAuthorizeFluxWhenDeniedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.fluxPostAuthorizeFindById(1))
.expectError(AccessDeniedException.class)
.verify();
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
}

@Test
@WithMockUser
void postAuthorizeFluxWhenGrantedThenPublishEvent() {
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
StepVerifier.create(this.messageService.fluxPostAuthorizeFindById(1)).expectNext("user").verifyComplete();
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
assertThat(event).isNotNull();
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
StepVerifier.create(event.getAuthentication()).expectNextCount(1).verifyComplete();
}

@Configuration
@EnableReactiveMethodSecurity
static class Config {

@Bean
DelegatingReactiveMessageService defaultMessageService() {
return new DelegatingReactiveMessageService(new StubReactiveMessageService());
}

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

}

@Configuration
static class AuthorizationEventPublisherConfig {

@Bean
ReactiveAuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher eventPublisher) {
return new ReactiveAuthorizationEventPublisher() {
@Override
public <T> void publishAuthorizationEvent(Mono<Authentication> authentication, T object,
AuthorizationDecision decision) {
ReactiveAuthorizationEvent event;
if (decision.isGranted()) {
event = new ReactiveAuthorizationGrantedEvent<>(authentication, object, decision);
}
else {
event = new ReactiveAuthorizationDeniedEvent<>(authentication, object, decision);
}
eventPublisher.publishEvent(event);
}
};
}

@Bean
MyEventListener myEventListener() {
return new MyEventListener();
}

}

public static class MyEventListener implements ApplicationListener<ReactiveAuthorizationEvent> {

static BlockingQueue<ReactiveAuthorizationEvent> events = new ArrayBlockingQueue<>(10);

public <T extends ReactiveAuthorizationEvent> T getEvent() {
try {
return (T) events.poll(1, TimeUnit.SECONDS);
}
catch (InterruptedException ex) {
return null;
}
}

@Override
public void onApplicationEvent(ReactiveAuthorizationEvent event) {
events.add(event);
}

}

}

0 comments on commit ada9549

Please sign in to comment.