From c312d181910b199ed3de35da33c2b9ac6e033241 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:54:34 -0600 Subject: [PATCH] Add Publishing Predicate Closes gh-17503 --- .../SpringAuthorizationEventPublisher.java | 22 ++++- ...pringAuthorizationEventPublisherTests.java | 16 ++++ .../pages/servlet/authorization/events.adoc | 87 +++++-------------- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java b/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java index dc9907d53d..1cae9b35ca 100644 --- a/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java +++ b/core/src/main/java/org/springframework/security/authorization/SpringAuthorizationEventPublisher.java @@ -16,6 +16,7 @@ package org.springframework.security.authorization; +import java.util.function.Predicate; import java.util.function.Supplier; import org.springframework.context.ApplicationEventPublisher; @@ -40,6 +41,8 @@ public final class SpringAuthorizationEventPublisher implements AuthorizationEve private final ApplicationEventPublisher eventPublisher; + private Predicate shouldPublishResult = (result) -> !result.isGranted(); + /** * Construct this publisher using Spring's {@link ApplicationEventPublisher} * @param eventPublisher @@ -55,11 +58,28 @@ public final class SpringAuthorizationEventPublisher implements AuthorizationEve @Override public void publishAuthorizationEvent(Supplier authentication, T object, AuthorizationResult result) { - if (result == null || result.isGranted()) { + if (result == null) { + return; + } + if (!this.shouldPublishResult.test(result)) { return; } AuthorizationDeniedEvent failure = new AuthorizationDeniedEvent<>(authentication, object, result); this.eventPublisher.publishEvent(failure); } + /** + * Use this predicate to test whether to publish an event. + * + *

+ * Since you cannot publish a {@code null} event, checking for null is already + * performed before this test is run + * @param shouldPublishResult the test to perform on non-{@code null} events + * @since 7.0 + */ + public void setShouldPublishResult(Predicate shouldPublishResult) { + Assert.notNull(shouldPublishResult, "shouldPublishResult cannot be null"); + this.shouldPublishResult = shouldPublishResult; + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java index 466e484c74..cdea1b9fae 100644 --- a/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java +++ b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java @@ -16,6 +16,7 @@ package org.springframework.security.authorization; +import java.util.function.Predicate; import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; @@ -26,10 +27,13 @@ import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.event.AuthorizationDeniedEvent; import org.springframework.security.core.Authentication; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; 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 SpringAuthorizationEventPublisher} @@ -64,4 +68,16 @@ public class SpringAuthorizationEventPublisherTests { verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationDeniedEvent.class)); } + @Test + public void publishWhenPredicateMatchesThenEvent() { + Predicate test = mock(Predicate.class); + given(test.test(any())).willReturn(true, false); + this.authorizationEventPublisher.setShouldPublishResult(test); + AuthorizationResult result = new AuthorizationDecision(false); + this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), result); + verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationDeniedEvent.class)); + this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), result); + verifyNoMoreInteractions(this.applicationEventPublisher); + } + } diff --git a/docs/modules/ROOT/pages/servlet/authorization/events.adoc b/docs/modules/ROOT/pages/servlet/authorization/events.adoc index 5d8fd796e9..2d25965626 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/events.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/events.adoc @@ -74,7 +74,7 @@ Because ``AuthorizationGrantedEvent``s have the potential to be quite noisy, the In fact, publishing these events will likely require some business logic on your part to ensure that your application is not inundated with noisy authorization events. -You can create your own event publisher that filters success events. +You can provide your own predicate that filters success events. For example, the following publisher only publishes authorization grants where `ROLE_ADMIN` was required: [tabs] @@ -83,44 +83,20 @@ Java:: + [source,java,role="primary"] ---- -@Component -public class MyAuthorizationEventPublisher implements AuthorizationEventPublisher { - private final ApplicationEventPublisher publisher; - private final AuthorizationEventPublisher delegate; - - public MyAuthorizationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - this.delegate = new SpringAuthorizationEventPublisher(publisher); - } - - @Override - public void publishAuthorizationEvent(Supplier authentication, - T object, AuthorizationResult result) { - if (result == null) { - return; - } +@Bean +AuthorizationEventPublisher authorizationEventPublisher() { + SpringAuthorizationEventPublisher eventPublisher = new SpringAuthorizationEventPublisher(); + eventPublisher.setShouldPublishEvent((result) -> { if (!result.isGranted()) { - this.delegate.publishAuthorizationEvent(authentication, object, result); - return; + return true; } - if (shouldThisEventBePublished(result)) { - AuthorizationGrantedEvent granted = new AuthorizationGrantedEvent( - authentication, object, result); - this.publisher.publishEvent(granted); - } - } - - private boolean shouldThisEventBePublished(AuthorizationResult result) { - if (result instanceof AuthorityAuthorizationDecision authorityAuthorizationDecision) { - Collection authorities = authorityAuthorizationDecision.getAuthorities(); - for (GrantedAuthority authority : authorities) { - if ("ROLE_ADMIN".equals(authority.getAuthority())) { - return true; - } - } + if (result instanceof AuthorityAuthorizationDecision decision) { + Collection authorities = decision.getAuthorities(); + return AuthorityUtils.authorityListToSet(authorities).contains("ROLE_ADMIN"); } return false; - } + }); + return eventPublisher; } ---- @@ -128,41 +104,20 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@Component -class MyAuthorizationEventPublisher(val publisher: ApplicationEventPublisher, - val delegate: SpringAuthorizationEventPublisher = SpringAuthorizationEventPublisher(publisher)): - AuthorizationEventPublisher { - - override fun publishAuthorizationEvent( - authentication: Supplier?, - `object`: T, - result: AuthorizationResult? - ) { - if (result == null) { - return +@Bean +fun authorizationEventPublisher(): AuthorizationEventPublisher { + val eventPublisher = SpringAuthorizationEventPublisher() + eventPublisher.setShouldPublishEvent { (result) -> + if (!result.isGranted()) { + return true } - if (!result.isGranted) { - this.delegate.publishAuthorizationEvent(authentication, `object`, result) - return - } - if (shouldThisEventBePublished(result)) { - val granted = AuthorizationGrantedEvent(authentication, `object`, result) - this.publisher.publishEvent(granted) - } - } - - private fun shouldThisEventBePublished(result: AuthorizationResult): Boolean { - if (decision !is AuthorityAuthorizationDecision) { - return false - } - val authorities = decision.authorities - for (authority in authorities) { - if ("ROLE_ADMIN" == authority.authority) { - return true - } + if (decision is AuthorityAuthorizationDecision) { + val authorities = decision.getAuthorities() + return AuthorityUtils.authorityListToSet(authorities).contains("ROLE_ADMIN") } return false } + return eventPublisher } ---- ======