From 0e2fc51bad2a4f246e5e0c4e88d0df10ae18734b Mon Sep 17 00:00:00 2001 From: David Herberth Date: Thu, 14 Apr 2022 17:13:21 +0200 Subject: [PATCH] Add DelegatingServerHttpHeadersWriter Servlet Spring Security has DelegatingRequestMatcherHeaderWriter the reactive world of Spring Security was missing a class to conditionally write headers. Closes gh-11073 --- ...angeDelegatingServerHttpHeadersWriter.java | 69 +++++++++++ ...elegatingServerHttpHeadersWriterTests.java | 111 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java create mode 100644 web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java diff --git a/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java new file mode 100644 index 0000000000..a62f4677d0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriter.java @@ -0,0 +1,69 @@ +/* + * 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.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Delegates to a provided {@link ServerHttpHeadersWriter} if + * {@link ServerWebExchangeMatcher#matches(ServerWebExchange)} returns a match. + * + * @author David Herberth + * @since 5.8 + */ +public final class ServerWebExchangeDelegatingServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + private final ServerWebExchangeMatcherEntry headersWriter; + + /** + * Creates a new instance + * @param headersWriter the {@link ServerWebExchangeMatcherEntry} holding a + * {@link ServerWebExchangeMatcher} and the {@link ServerHttpHeadersWriter} to invoke + * if the matcher returns a match. + */ + public ServerWebExchangeDelegatingServerHttpHeadersWriter( + ServerWebExchangeMatcherEntry headersWriter) { + Assert.notNull(headersWriter, "headersWriter cannot be null"); + this.headersWriter = headersWriter; + } + + /** + * Creates a new instance + * @param webExchangeMatcher the {@link ServerWebExchangeMatcher} to use. If it + * returns a match, the delegateHeadersWriter is invoked. + * @param delegateHeadersWriter the {@link ServerHttpHeadersWriter} to invoke if the + * {@link ServerWebExchangeMatcher} returns a match. + */ + public ServerWebExchangeDelegatingServerHttpHeadersWriter(ServerWebExchangeMatcher webExchangeMatcher, + ServerHttpHeadersWriter delegateHeadersWriter) { + Assert.notNull(webExchangeMatcher, "webExchangeMatcher cannot be null"); + Assert.notNull(delegateHeadersWriter, "delegateHeadersWriter cannot be null"); + this.headersWriter = new ServerWebExchangeMatcherEntry<>(webExchangeMatcher, delegateHeadersWriter); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return this.headersWriter.getMatcher().matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .flatMap((matchResult) -> this.headersWriter.getEntry().writeHttpHeaders(exchange)); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..d2858e7585 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/ServerWebExchangeDelegatingServerHttpHeadersWriterTests.java @@ -0,0 +1,111 @@ +/* + * 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.web.server.header; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author David Herberth + */ +@ExtendWith(MockitoExtension.class) +public class ServerWebExchangeDelegatingServerHttpHeadersWriterTests { + + @Mock + private ServerWebExchangeMatcher matcher; + + @Mock + private ServerHttpHeadersWriter delegate; + + @Mock + private ServerWebExchangeMatcherEntry matcherEntry; + + private ServerWebExchange exchange; + + private ServerWebExchangeDelegatingServerHttpHeadersWriter headerWriter; + + @BeforeEach + public void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.headerWriter = new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.matcher, this.delegate); + } + + @Test + public void constructorNullWebExchangeMatcher() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(null, this.delegate)); + } + + @Test + public void constructorNullWebExchangeMatcherEntry() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(null)); + } + + @Test + public void constructorNullDelegate() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.matcher, null)); + } + + @Test + public void writeHeadersOnMatch() { + Map params = Collections.singletonMap("foo", "bar"); + given(this.matcher.matches(this.exchange)).willReturn(ServerWebExchangeMatcher.MatchResult.match(params)); + given(this.delegate.writeHttpHeaders(this.exchange)).willReturn(Mono.empty()); + this.headerWriter.writeHttpHeaders(this.exchange).block(); + verify(this.delegate).writeHttpHeaders(this.exchange); + } + + @Test + public void writeHeadersOnNoMatch() { + given(this.matcher.matches(this.exchange)).willReturn(ServerWebExchangeMatcher.MatchResult.notMatch()); + this.headerWriter.writeHttpHeaders(this.exchange).block(); + verify(this.delegate, times(0)).writeHttpHeaders(this.exchange); + } + + @Test + public void writeHeadersOnMatchWithServerWebExchangeMatcherEntry() { + this.headerWriter = new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.matcherEntry); + given(this.matcherEntry.getMatcher()).willReturn(this.matcher); + given(this.matcherEntry.getEntry()).willReturn(this.delegate); + Map params = Collections.singletonMap("foo", "bar"); + given(this.matcher.matches(this.exchange)).willReturn(ServerWebExchangeMatcher.MatchResult.match(params)); + given(this.delegate.writeHttpHeaders(this.exchange)).willReturn(Mono.empty()); + this.headerWriter.writeHttpHeaders(this.exchange).block(); + verify(this.delegate).writeHttpHeaders(this.exchange); + } + +}