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; package org.springframework.security.authorization;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
@ -40,6 +41,8 @@ public final class SpringAuthorizationEventPublisher implements AuthorizationEve
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private Predicate<AuthorizationResult> shouldPublishResult = (result) -> !result.isGranted();
/** /**
* Construct this publisher using Spring's {@link ApplicationEventPublisher} * Construct this publisher using Spring's {@link ApplicationEventPublisher}
* @param eventPublisher * @param eventPublisher
@ -55,11 +58,28 @@ public final class SpringAuthorizationEventPublisher implements AuthorizationEve
@Override @Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object, public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationResult result) { AuthorizationResult result) {
if (result == null || result.isGranted()) { if (result == null) {
return;
}
if (!this.shouldPublishResult.test(result)) {
return; return;
} }
AuthorizationDeniedEvent<T> failure = new AuthorizationDeniedEvent<>(authentication, object, result); AuthorizationDeniedEvent<T> failure = new AuthorizationDeniedEvent<>(authentication, object, result);
this.eventPublisher.publishEvent(failure); 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; package org.springframework.security.authorization;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach; 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.authorization.event.AuthorizationDeniedEvent;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/** /**
* Tests for {@link SpringAuthorizationEventPublisher} * Tests for {@link SpringAuthorizationEventPublisher}
@ -64,4 +68,16 @@ public class SpringAuthorizationEventPublisherTests {
verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationDeniedEvent.class)); 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. 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: For example, the following publisher only publishes authorization grants where `ROLE_ADMIN` was required:
[tabs] [tabs]
@ -83,44 +83,20 @@ Java::
+ +
[source,java,role="primary"] [source,java,role="primary"]
---- ----
@Component @Bean
public class MyAuthorizationEventPublisher implements AuthorizationEventPublisher { AuthorizationEventPublisher authorizationEventPublisher() {
private final ApplicationEventPublisher publisher; SpringAuthorizationEventPublisher eventPublisher = new SpringAuthorizationEventPublisher();
private final AuthorizationEventPublisher delegate; eventPublisher.setShouldPublishEvent((result) -> {
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;
}
if (!result.isGranted()) { if (!result.isGranted()) {
this.delegate.publishAuthorizationEvent(authentication, object, result); return true;
return;
} }
if (shouldThisEventBePublished(result)) { if (result instanceof AuthorityAuthorizationDecision decision) {
AuthorizationGrantedEvent granted = new AuthorizationGrantedEvent( Collection<GrantedAuthority> authorities = decision.getAuthorities();
authentication, object, result); return AuthorityUtils.authorityListToSet(authorities).contains("ROLE_ADMIN");
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;
}
}
} }
return false; return false;
} });
return eventPublisher;
} }
---- ----
@ -128,41 +104,20 @@ Kotlin::
+ +
[source,kotlin,role="secondary"] [source,kotlin,role="secondary"]
---- ----
@Component @Bean
class MyAuthorizationEventPublisher(val publisher: ApplicationEventPublisher, fun authorizationEventPublisher(): AuthorizationEventPublisher {
val delegate: SpringAuthorizationEventPublisher = SpringAuthorizationEventPublisher(publisher)): val eventPublisher = SpringAuthorizationEventPublisher()
AuthorizationEventPublisher { eventPublisher.setShouldPublishEvent { (result) ->
if (!result.isGranted()) {
override fun <T : Any?> publishAuthorizationEvent( return true
authentication: Supplier<Authentication>?,
`object`: T,
result: AuthorizationResult?
) {
if (result == null) {
return
} }
if (!result.isGranted) { if (decision is AuthorityAuthorizationDecision) {
this.delegate.publishAuthorizationEvent(authentication, `object`, result) val authorities = decision.getAuthorities()
return return AuthorityUtils.authorityListToSet(authorities).contains("ROLE_ADMIN")
}
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
}
} }
return false return false
} }
return eventPublisher
} }
---- ----
====== ======