From c004addfeeb83d00b349892243126b9cdc16d12b Mon Sep 17 00:00:00 2001 From: Parikshit Dutta Date: Tue, 30 Mar 2021 03:31:46 +0530 Subject: [PATCH] added support for authorization events in DelegatingAuthorizationManager --- .../AuthorizationEventPublisher.java | 29 ++++++++ .../DefaultAuthorizationEventPublisher.java | 61 ++++++++++++++++ .../event/AuthorizationFailureEvent.java | 34 +++++++++ .../event/AuthorizationSuccessEvent.java | 34 +++++++++ ...faultAuthorizationEventPublisherTests.java | 70 +++++++++++++++++++ .../DelegatingAuthorizationManager.java | 30 +++++++- .../DelegatingAuthorizationManagerTests.java | 42 ++++++++++- 7 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationEventPublisher.java create mode 100644 core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisher.java create mode 100644 core/src/main/java/org/springframework/security/authorization/event/AuthorizationFailureEvent.java create mode 100644 core/src/main/java/org/springframework/security/authorization/event/AuthorizationSuccessEvent.java create mode 100644 core/src/test/java/org/springframework/security/authorization/DefaultAuthorizationEventPublisherTests.java 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 00000000000..4a4cc4b22d4 --- /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 00000000000..7e2079e4e88 --- /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 00000000000..d1f7e6327b5 --- /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 00000000000..00f82b0ea7e --- /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 00000000000..a226de24aab --- /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.Before; +import org.junit.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; + + @Before + 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/DelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java index 705fb0efa24..6f35a23da58 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.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. @@ -27,6 +27,7 @@ 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; @@ -38,6 +39,7 @@ * {@link AuthorizationManager} based on a {@link RequestMatcher} evaluation. * * @author Evgeniy Cheban + * @author Parikshit Dutta * @since 5.5 */ public final class DelegatingAuthorizationManager implements AuthorizationManager { @@ -46,6 +48,8 @@ public final class DelegatingAuthorizationManager implements AuthorizationManage private final Map> mappings; + private AuthorizationEventPublisher authorizationEventPublisher; + private DelegatingAuthorizationManager( Map> mappings) { Assert.notEmpty(mappings, "mappings cannot be empty"); @@ -76,14 +80,36 @@ public AuthorizationDecision check(Supplier authentication, Http 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 DelegatingAuthorizationManager}. * @return the new {@link Builder} instance diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java index 04fa818b39d..0589aa53531 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.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. @@ -23,16 +23,20 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.TestingAuthenticationToken; 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 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 DelegatingAuthorizationManager}. * * @author Evgeniy Cheban + * @author Parikshit Dutta */ public class DelegatingAuthorizationManagerTests { @@ -81,4 +85,40 @@ public void checkWhenMultipleMappingsConfiguredThenDelegatesMatchingManager() { assertThat(abstain).isNull(); } + @Test + public void testAuthorizationEventPublisherIsNotNull() { + DelegatingAuthorizationManager manager = DelegatingAuthorizationManager.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() { + DelegatingAuthorizationManager manager = DelegatingAuthorizationManager.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() { + DelegatingAuthorizationManager manager = DelegatingAuthorizationManager.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); + } + }