Add Publishing Predicate

Closes gh-17503
This commit is contained in:
Josh Cummings 2025-07-07 17:54:34 -06:00
parent 901b386ca6
commit c312d18191
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
3 changed files with 58 additions and 67 deletions

View File

@ -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<AuthorizationResult> 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 <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationResult result) {
if (result == null || result.isGranted()) {
if (result == null) {
return;
}
if (!this.shouldPublishResult.test(result)) {
return;
}
AuthorizationDeniedEvent<T> failure = new AuthorizationDeniedEvent<>(authentication, object, result);
this.eventPublisher.publishEvent(failure);
}
/**
* Use this predicate to test whether to publish an event.
*
* <p>
* 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<AuthorizationResult> shouldPublishResult) {
Assert.notNull(shouldPublishResult, "shouldPublishResult cannot be null");
this.shouldPublishResult = shouldPublishResult;
}
}

View File

@ -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<AuthorizationResult> 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);
}
}

View File

@ -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 <T> void publishAuthorizationEvent(Supplier<Authentication> 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<GrantedAuthority> authorities = authorityAuthorizationDecision.getAuthorities();
for (GrantedAuthority authority : authorities) {
if ("ROLE_ADMIN".equals(authority.getAuthority())) {
return true;
}
}
if (result instanceof AuthorityAuthorizationDecision decision) {
Collection<GrantedAuthority> 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 <T : Any?> publishAuthorizationEvent(
authentication: Supplier<Authentication>?,
`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
}
----
======