1
0
mirror of synced 2026-08-05 17:57:15 +00:00

Polish Authorization Event Support

- Added spring-security-config support
- Renamed classes
- Changed contracts to include the authenticated user and secured
object
- Added method security support

Issue gh-9288
This commit is contained in:
Josh Cummings
2022-03-29 11:52:08 -06:00
parent bd9434882f
commit 061f69eb70
19 changed files with 498 additions and 239 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,14 +16,34 @@
package org.springframework.security.authorization;
import java.util.function.Supplier;
import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
import org.springframework.security.core.Authentication;
/**
* A contract for publishing authorization events
*
* @author Parikshit Dutta
* @since 5.5
* @author Josh Cummings
* @since 5.7
* @see AuthorizationManager
*/
public interface AuthorizationEventPublisher {
void publishAuthorizationSuccess(AuthorizationDecision authorizationDecision);
void publishAuthorizationFailure(AuthorizationDecision authorizationDecision);
/**
* Publish the given details in the form of an event, typically
* {@link AuthorizationGrantedEvent} or {@link AuthorizationDeniedEvent}.
*
* Note that success events can be very noisy if enabled by default. Because of this
* implementations may choose to drop success events by default.
* @param authentication a {@link Supplier} for the current user
* @param object the secured object
* @param decision the decision about whether the user may access the secured object
* @param <T> the secured object's type
*/
<T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision);
}
@@ -1,61 +0,0 @@
/*
* Copyright 2002-2021 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.authorization;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.authorization.event.AuthorizationFailureEvent;
import org.springframework.security.authorization.event.AuthorizationSuccessEvent;
/**
* Default implementation of {@link AuthorizationEventPublisher}
*
* @author Parikshit Dutta
* @since 5.5
*/
public class DefaultAuthorizationEventPublisher implements AuthorizationEventPublisher, ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
public DefaultAuthorizationEventPublisher() {
this(null);
}
public DefaultAuthorizationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void publishAuthorizationSuccess(AuthorizationDecision authorizationDecision) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new AuthorizationSuccessEvent(authorizationDecision));
}
}
@Override
public void publishAuthorizationFailure(AuthorizationDecision authorizationDecision) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new AuthorizationFailureEvent(authorizationDecision));
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2022 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.authorization;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An implementation of {@link AuthorizationEventPublisher} that uses Spring's event
* publishing support.
*
* Because {@link AuthorizationGrantedEvent}s typically require additional business logic
* to decide whether to publish, this implementation only publishes
* {@link AuthorizationDeniedEvent}s.
*
* @author Parikshit Dutta
* @author Josh Cummings
* @since 5.7
*/
public final class SpringAuthorizationEventPublisher implements AuthorizationEventPublisher {
private final ApplicationEventPublisher eventPublisher;
/**
* Construct this publisher using Spring's {@link ApplicationEventPublisher}
* @param eventPublisher
*/
public SpringAuthorizationEventPublisher(ApplicationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* {@inheritDoc}
*/
@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
if (decision == null || decision.isGranted()) {
return;
}
AuthorizationDeniedEvent<T> failure = new AuthorizationDeniedEvent<>(authentication, object, decision);
this.eventPublisher.publishEvent(failure);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,19 +16,37 @@
package org.springframework.security.authorization.event;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
/**
* An {@link ApplicationEvent} which indicates failed authorization.
*
* @author Parikshit Dutta
* @since 5.5
* @author Josh Cummings
* @since 5.7
*/
public class AuthorizationFailureEvent extends ApplicationEvent {
public class AuthorizationDeniedEvent<T> extends ApplicationEvent {
public AuthorizationFailureEvent(AuthorizationDecision authorizationDecision) {
super(authorizationDecision);
private final Supplier<Authentication> authentication;
private final AuthorizationDecision decision;
public AuthorizationDeniedEvent(Supplier<Authentication> authentication, T object, AuthorizationDecision decision) {
super(object);
this.authentication = authentication;
this.decision = decision;
}
public Supplier<Authentication> getAuthentication() {
return this.authentication;
}
public AuthorizationDecision getAuthorizationDecision() {
return this.decision;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,19 +16,40 @@
package org.springframework.security.authorization.event;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link ApplicationEvent} which indicates successful authorization.
*
* @author Parikshit Dutta
* @since 5.5
* @author Josh Cummings
* @since 5.7
*/
public class AuthorizationSuccessEvent extends ApplicationEvent {
public class AuthorizationGrantedEvent<T> extends ApplicationEvent {
public AuthorizationSuccessEvent(AuthorizationDecision authorizationDecision) {
super(authorizationDecision);
private final Supplier<Authentication> authentication;
private final AuthorizationDecision decision;
public AuthorizationGrantedEvent(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
super(object);
Assert.notNull(authentication, "authentication supplier cannot be null");
this.authentication = authentication;
this.decision = decision;
}
public Supplier<Authentication> getAuthentication() {
return this.authentication;
}
public AuthorizationDecision getAuthorizationDecision() {
return this.decision;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -33,6 +33,7 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -66,6 +67,8 @@ public final class AuthorizationManagerAfterMethodInterceptor
private int order;
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish;
/**
* Creates an instance.
* @param pointcut the {@link Pointcut} to use
@@ -122,6 +125,17 @@ public final class AuthorizationManagerAfterMethodInterceptor
this.order = order;
}
/**
* Use this {@link AuthorizationEventPublisher} to publish the
* {@link AuthorizationManager} result.
* @param eventPublisher
* @since 5.7
*/
public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* {@inheritDoc}
*/
@@ -142,8 +156,9 @@ public final class AuthorizationManagerAfterMethodInterceptor
private void attemptAuthorization(MethodInvocation mi, Object result) {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER,
new MethodInvocationResult(mi, result));
MethodInvocationResult object = new MethodInvocationResult(mi, result);
AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, object);
this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, object, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
@@ -152,4 +167,9 @@ public final class AuthorizationManagerAfterMethodInterceptor
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
}
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -38,6 +38,7 @@ import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -71,6 +72,8 @@ public final class AuthorizationManagerBeforeMethodInterceptor
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish;
/**
* Creates an instance.
* @param pointcut the {@link Pointcut} to use
@@ -168,6 +171,17 @@ public final class AuthorizationManagerBeforeMethodInterceptor
this.order = order;
}
/**
* Use this {@link AuthorizationEventPublisher} to publish the
* {@link AuthorizationManager} result.
* @param eventPublisher
* @since 5.7
*/
public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* {@inheritDoc}
*/
@@ -189,6 +203,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor
private void attemptAuthorization(MethodInvocation mi) {
this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi));
AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi);
this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, mi, decision);
if (decision != null && !decision.isGranted()) {
this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager "
+ this.authorizationManager + " and decision " + decision));
@@ -197,4 +212,9 @@ public final class AuthorizationManagerBeforeMethodInterceptor
this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi));
}
private static <T> void noPublish(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
}
}
@@ -1,70 +0,0 @@
/*
* Copyright 2002-2021 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.authorization;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authorization.event.AuthorizationFailureEvent;
import org.springframework.security.authorization.event.AuthorizationSuccessEvent;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DefaultAuthorizationEventPublisher}
*
* @author Parikshit Dutta
*/
public class DefaultAuthorizationEventPublisherTests {
ApplicationEventPublisher applicationEventPublisher;
DefaultAuthorizationEventPublisher authorizationEventPublisher;
@BeforeEach
public void init() {
this.applicationEventPublisher = mock(ApplicationEventPublisher.class);
this.authorizationEventPublisher = new DefaultAuthorizationEventPublisher();
this.authorizationEventPublisher.setApplicationEventPublisher(this.applicationEventPublisher);
}
@Test
public void testAuthenticationSuccessIsPublished() {
this.authorizationEventPublisher.publishAuthorizationSuccess(mock(AuthorizationDecision.class));
verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationSuccessEvent.class));
}
@Test
public void testAuthenticationFailureIsPublished() {
this.authorizationEventPublisher.publishAuthorizationFailure(mock(AuthorizationDecision.class));
verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationFailureEvent.class));
}
@Test
public void testNullPublisherNotInvoked() {
this.authorizationEventPublisher.setApplicationEventPublisher(null);
this.authorizationEventPublisher.publishAuthorizationSuccess(mock(AuthorizationDecision.class));
this.authorizationEventPublisher.publishAuthorizationFailure(mock(AuthorizationDecision.class));
verify(this.applicationEventPublisher, never()).publishEvent(any());
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2022 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.authorization;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
import org.springframework.security.core.Authentication;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link SpringAuthorizationEventPublisher}
*
* @author Parikshit Dutta
*/
public class SpringAuthorizationEventPublisherTests {
Supplier<Authentication> authentication = () -> TestAuthentication.authenticatedUser();
ApplicationEventPublisher applicationEventPublisher;
SpringAuthorizationEventPublisher authorizationEventPublisher;
@BeforeEach
public void init() {
this.applicationEventPublisher = mock(ApplicationEventPublisher.class);
this.authorizationEventPublisher = new SpringAuthorizationEventPublisher(this.applicationEventPublisher);
}
@Test
public void testAuthenticationSuccessIsNotPublished() {
AuthorizationDecision decision = new AuthorizationDecision(true);
this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), decision);
verifyNoInteractions(this.applicationEventPublisher);
}
@Test
public void testAuthenticationFailureIsPublished() {
AuthorizationDecision decision = new AuthorizationDecision(false);
this.authorizationEventPublisher.publishAuthorizationEvent(this.authentication, mock(Object.class), decision);
verify(this.applicationEventPublisher).publishEvent(isA(AuthorizationDeniedEvent.class));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,11 +16,20 @@
package org.springframework.security.authorization.method;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -66,4 +75,32 @@ public class AuthorizationManagerAfterMethodInterceptorTests {
any(MethodInvocationResult.class));
}
@Test
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated());
assertThatIllegalArgumentException().isThrownBy(() -> advice.setAuthorizationEventPublisher(null))
.withMessage("eventPublisher cannot be null");
}
@Test
public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable {
AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated());
AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class);
advice.setAuthorizationEventPublisher(eventPublisher);
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
SecurityContextHolder.setContext(securityContext);
MethodInvocation mockMethodInvocation = mock(MethodInvocation.class);
MethodInvocationResult result = new MethodInvocationResult(mockMethodInvocation, new Object());
given(mockMethodInvocation.proceed()).willReturn(result.getResult());
advice.invoke(mockMethodInvocation);
verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocationResult.class),
any(AuthorizationDecision.class));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -16,13 +16,24 @@
package org.springframework.security.authorization.method;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -59,4 +70,32 @@ public class AuthorizationManagerBeforeMethodInterceptorTests {
mockMethodInvocation);
}
@Test
public void configureWhenAuthorizationEventPublisherIsNullThenIllegalArgument() {
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated());
assertThatIllegalArgumentException().isThrownBy(() -> advice.setAuthorizationEventPublisher(null))
.withMessage("eventPublisher cannot be null");
}
@Test
public void invokeWhenAuthorizationEventPublisherThenUses() throws Throwable {
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
Pointcut.TRUE, AuthenticatedAuthorizationManager.authenticated());
AuthorizationEventPublisher eventPublisher = mock(AuthorizationEventPublisher.class);
advice.setAuthorizationEventPublisher(eventPublisher);
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
SecurityContextHolder.setContext(securityContext);
MethodInvocation mockMethodInvocation = mock(MethodInvocation.class);
MethodInvocationResult result = new MethodInvocationResult(mockMethodInvocation, new Object());
given(mockMethodInvocation.proceed()).willReturn(result.getResult());
advice.invoke(mockMethodInvocation);
verify(eventPublisher).publishAuthorizationEvent(any(Supplier.class), any(MethodInvocation.class),
any(AuthorizationDecision.class));
}
}