diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java new file mode 100644 index 0000000000..4a4cc4b22d --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * @author Parikshit Dutta + * @since 5.5 + */ +public interface AuthorizationEventPublisher { + + void publishAuthorizationSuccess(AuthorizationDecision authorizationDecision); + + void publishAuthorizationFailure(AuthorizationDecision authorizationDecision); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisher.java b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisher.java new file mode 100644 index 0000000000..7e2079e4e8 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisher.java @@ -0,0 +1,61 @@ +/* + * 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)); + } + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/event/AuthorizationFailureEvent.java b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationFailureEvent.java new file mode 100644 index 0000000000..d1f7e6327b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationFailureEvent.java @@ -0,0 +1,34 @@ +/* + * 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.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.security.authorization.AuthorizationDecision; + +/** + * An {@link ApplicationEvent} which indicates failed authorization. + * + * @author Parikshit Dutta + * @since 5.5 + */ +public class AuthorizationFailureEvent extends ApplicationEvent { + + public AuthorizationFailureEvent(AuthorizationDecision authorizationDecision) { + super(authorizationDecision); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/event/AuthorizationSuccessEvent.java b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationSuccessEvent.java new file mode 100644 index 0000000000..00f82b0ea7 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/event/AuthorizationSuccessEvent.java @@ -0,0 +1,34 @@ +/* + * 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.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.security.authorization.AuthorizationDecision; + +/** + * An {@link ApplicationEvent} which indicates successful authorization. + * + * @author Parikshit Dutta + * @since 5.5 + */ +public class AuthorizationSuccessEvent extends ApplicationEvent { + + public AuthorizationSuccessEvent(AuthorizationDecision authorizationDecision) { + super(authorizationDecision); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisherTests.java b/core/src/test/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisherTests.java new file mode 100644 index 0000000000..d2e9131bab --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisherTests.java @@ -0,0 +1,70 @@ +/* + * 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()); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index 066bac5e69..f751658788 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; 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.web.util.matcher.RequestMatcher; @@ -39,6 +40,7 @@ import org.springframework.util.Assert; * {@link AuthorizationManager} based on a {@link RequestMatcher} evaluation. * * @author Evgeniy Cheban + * @author Parikshit Dutta * @since 5.5 */ public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager { @@ -47,6 +49,8 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho private final Map> mappings; + private AuthorizationEventPublisher authorizationEventPublisher; + private RequestMatcherDelegatingAuthorizationManager( Map> mappings) { Assert.notEmpty(mappings, "mappings cannot be empty"); @@ -77,14 +81,36 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager)); } - return manager.check(authentication, + AuthorizationDecision authorizationDecision = manager.check(authentication, new RequestAuthorizationContext(request, matchResult.getVariables())); + publishAuthorizationEvent(authorizationDecision); + return authorizationDecision; } } this.logger.trace("Abstaining since did not find matching RequestMatcher"); return null; } + private void publishAuthorizationEvent(AuthorizationDecision authorizationDecision) { + if (this.authorizationEventPublisher != null) { + if (authorizationDecision.isGranted()) { + this.authorizationEventPublisher.publishAuthorizationSuccess(authorizationDecision); + } + else { + this.authorizationEventPublisher.publishAuthorizationFailure(authorizationDecision); + } + } + } + + /** + * Set implementation of an {@link AuthorizationEventPublisher} + * @param authorizationEventPublisher + */ + public void setAuthorizationEventPublisher(AuthorizationEventPublisher authorizationEventPublisher) { + Assert.notNull(authorizationEventPublisher, "AuthorizationEventPublisher cannot be null"); + this.authorizationEventPublisher = authorizationEventPublisher; + } + /** * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}. * @return the new {@link Builder} instance diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java index 952132522b..44d49e2789 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -24,17 +24,21 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link RequestMatcherDelegatingAuthorizationManager}. * * @author Evgeniy Cheban + * @author Parikshit Dutta */ public class RequestMatcherDelegatingAuthorizationManagerTests { @@ -98,6 +102,7 @@ public class RequestMatcherDelegatingAuthorizationManagerTests { Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/grant")); + assertThat(grant).isNotNull(); assertThat(grant.isGranted()).isTrue(); @@ -121,4 +126,40 @@ public class RequestMatcherDelegatingAuthorizationManagerTests { .withMessage("mappingsConsumer cannot be null"); } + @Test + public void testAuthorizationEventPublisherIsNotNull() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(new MvcRequestMatcher(null, "/grant"), (a, o) -> new AuthorizationDecision(true)).build(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setAuthorizationEventPublisher(null)) + .withMessage("AuthorizationEventPublisher cannot be null"); + } + + @Test + public void testAuthorizationSuccessEventWhenAuthorizationGranted() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(new MvcRequestMatcher(null, "/grant"), (a, o) -> new AuthorizationDecision(true)).build(); + + AuthorizationEventPublisher authorizationEventPublisher = mock(AuthorizationEventPublisher.class); + manager.setAuthorizationEventPublisher(authorizationEventPublisher); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/grant")); + verify(authorizationEventPublisher).publishAuthorizationSuccess(grant); + } + + @Test + public void testAuthorizationFailureEventWhenAuthorizationNotGranted() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(new MvcRequestMatcher(null, "/deny"), (a, o) -> new AuthorizationDecision(false)).build(); + + AuthorizationEventPublisher authorizationEventPublisher = mock(AuthorizationEventPublisher.class); + manager.setAuthorizationEventPublisher(authorizationEventPublisher); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/deny")); + verify(authorizationEventPublisher).publishAuthorizationFailure(grant); + } + }