浏览代码

Add authorization events

Closes gh-9288
Parikshit Dutta 4 年之前
父节点
当前提交
990831db85

+ 29 - 0
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);
+
+}

+ 61 - 0
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));
+		}
+	}
+
+}

+ 34 - 0
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);
+	}
+
+}

+ 34 - 0
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);
+	}
+
+}

+ 70 - 0
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());
+	}
+
+}

+ 27 - 1
web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java

@@ -27,6 +27,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;
@@ -38,6 +39,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<HttpServletRequest> {
@@ -46,6 +48,8 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho
 
 	private final Map<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>> mappings;
 
+	private AuthorizationEventPublisher authorizationEventPublisher;
+
 	private RequestMatcherDelegatingAuthorizationManager(
 			Map<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>> mappings) {
 		Assert.notEmpty(mappings, "mappings cannot be empty");
@@ -76,14 +80,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

+ 42 - 1
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> 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> 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> authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+
+		AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/deny"));
+		verify(authorizationEventPublisher).publishAuthorizationFailure(grant);
+	}
+
 }