Quellcode durchsuchen

Support IP whitelist for Spring Security Webflux

Closes gh-7765
Guirong Hu vor 4 Jahren
Ursprung
Commit
43317c5a61

+ 12 - 0
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -133,6 +133,7 @@ import org.springframework.security.web.server.authorization.AuthorizationContex
 import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
 import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager;
 import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
+import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager;
 import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
 import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;
 import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
@@ -1682,6 +1683,17 @@ public class ServerHttpSecurity {
 				return access(AuthenticatedReactiveAuthorizationManager.authenticated());
 			}
 
+			/**
+			 * Require a specific IP address or range using an IP/Netmask (e.g.
+			 * 192.168.1.0/24).
+			 * @param ipAddress the address or range of addresses from which the request
+			 * must come.
+			 * @return the {@link AuthorizeExchangeSpec} to configure
+			 */
+			public AuthorizeExchangeSpec hasIpAddress(String ipAddress) {
+				return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress));
+			}
+
 			/**
 			 * Allows plugging in a custom authorization strategy
 			 * @param manager the authorization manager to use

+ 59 - 0
web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java

@@ -0,0 +1,59 @@
+/*
+ * 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.web.server.authorization;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.util.matcher.IpAddressServerWebExchangeMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link ReactiveAuthorizationManager}, that determines if the current request contains
+ * the specified address or range of addresses
+ *
+ * @author Guirong Hu
+ * @since 5.7
+ */
+public final class IpAddressReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
+
+	private final IpAddressServerWebExchangeMatcher ipAddressExchangeMatcher;
+
+	IpAddressReactiveAuthorizationManager(String ipAddress) {
+		this.ipAddressExchangeMatcher = new IpAddressServerWebExchangeMatcher(ipAddress);
+	}
+
+	@Override
+	public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
+		return Mono.just(context.getExchange()).flatMap(this.ipAddressExchangeMatcher::matches)
+				.map((matchResult) -> new AuthorizationDecision(matchResult.isMatch()));
+	}
+
+	/**
+	 * Creates an instance of {@link IpAddressReactiveAuthorizationManager} with the
+	 * provided IP address.
+	 * @param ipAddress the address or range of addresses from which the request must
+	 * @return the new instance
+	 */
+	public static IpAddressReactiveAuthorizationManager hasIpAddress(String ipAddress) {
+		Assert.notNull(ipAddress, "This IP address is required; it must not be null");
+		return new IpAddressReactiveAuthorizationManager(ipAddress);
+	}
+
+}

+ 63 - 0
web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java

@@ -0,0 +1,63 @@
+/*
+ * 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.web.server.util.matcher;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.web.util.matcher.IpAddressMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Matches a request based on IP Address or subnet mask matching against the remote
+ * address.
+ *
+ * @author Guirong Hu
+ * @since 5.7
+ */
+public class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher {
+
+	private final IpAddressMatcher ipAddressMatcher;
+
+	/**
+	 * Takes a specific IP address or a range specified using the IP/Netmask (e.g.
+	 * 192.168.1.0/24 or 202.24.0.0/14).
+	 * @param ipAddress the address or range of addresses from which the request must
+	 * come.
+	 */
+	public IpAddressServerWebExchangeMatcher(String ipAddress) {
+		Assert.hasText(ipAddress, "IP address cannot be empty");
+		this.ipAddressMatcher = new IpAddressMatcher(ipAddress);
+	}
+
+	@Override
+	public Mono<MatchResult> matches(ServerWebExchange exchange) {
+		// @formatter:off
+		return Mono.justOrEmpty(exchange.getRequest().getRemoteAddress())
+				.map((remoteAddress) -> remoteAddress.getAddress().getHostAddress())
+				.map(this.ipAddressMatcher::matches)
+				.flatMap((matches) -> matches ? MatchResult.match() : MatchResult.notMatch())
+				.switchIfEmpty(MatchResult.notMatch());
+		// @formatter:on
+	}
+
+	@Override
+	public String toString() {
+		return "IpAddressServerWebExchangeMatcher{ipAddressMatcher=" + this.ipAddressMatcher + '}';
+	}
+
+}

+ 75 - 0
web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java

@@ -0,0 +1,75 @@
+/*
+ * 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.web.server.authorization;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link IpAddressReactiveAuthorizationManager}
+ *
+ * @author Guirong Hu
+ */
+public class IpAddressReactiveAuthorizationManagerTests {
+
+	@Test
+	public void checkWhenHasIpv6AddressThenReturnTrue() throws UnknownHostException {
+		IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager
+				.hasIpAddress("fe80::21f:5bff:fe33:bd68");
+		boolean granted = v6manager.check(null, context("fe80::21f:5bff:fe33:bd68")).block().isGranted();
+		assertThat(granted).isTrue();
+	}
+
+	@Test
+	public void checkWhenHasIpv6AddressThenReturnFalse() throws UnknownHostException {
+		IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager
+				.hasIpAddress("fe80::21f:5bff:fe33:bd68");
+		boolean granted = v6manager.check(null, context("fe80::1c9a:7cfd:29a8:a91e")).block().isGranted();
+		assertThat(granted).isFalse();
+	}
+
+	@Test
+	public void checkWhenHasIpv4AddressThenReturnTrue() throws UnknownHostException {
+		IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager
+				.hasIpAddress("192.168.1.104");
+		boolean granted = v4manager.check(null, context("192.168.1.104")).block().isGranted();
+		assertThat(granted).isTrue();
+	}
+
+	@Test
+	public void checkWhenHasIpv4AddressThenReturnFalse() throws UnknownHostException {
+		IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager
+				.hasIpAddress("192.168.1.104");
+		boolean granted = v4manager.check(null, context("192.168.100.15")).block().isGranted();
+		assertThat(granted).isFalse();
+	}
+
+	private static AuthorizationContext context(String ipAddress) throws UnknownHostException {
+		MockServerWebExchange exchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")
+				.remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build();
+		return new AuthorizationContext(exchange);
+	}
+
+}

+ 126 - 0
web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java

@@ -0,0 +1,126 @@
+/*
+ * 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.web.server.util.matcher;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link IpAddressServerWebExchangeMatcher}
+ *
+ * @author Guirong Hu
+ */
+@ExtendWith(MockitoExtension.class)
+public class IpAddressServerWebExchangeMatcherTests {
+
+	@Test
+	public void matchesWhenIpv6RangeAndIpv6AddressThenTrue() throws UnknownHostException {
+		ServerWebExchange ipv6Exchange = exchange("fe80::21f:5bff:fe33:bd68");
+		ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68")
+				.matches(ipv6Exchange).block();
+		assertThat(matches.isMatch()).isTrue();
+	}
+
+	@Test
+	public void matchesWhenIpv6RangeAndIpv4AddressThenFalse() throws UnknownHostException {
+		ServerWebExchange ipv4Exchange = exchange("192.168.1.104");
+		ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68")
+				.matches(ipv4Exchange).block();
+		assertThat(matches.isMatch()).isFalse();
+	}
+
+	@Test
+	public void matchesWhenIpv4RangeAndIpv4AddressThenTrue() throws UnknownHostException {
+		ServerWebExchange ipv4Exchange = exchange("192.168.1.104");
+		ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("192.168.1.104")
+				.matches(ipv4Exchange).block();
+		assertThat(matches.isMatch()).isTrue();
+	}
+
+	@Test
+	public void matchesWhenIpv4SubnetAndIpv4AddressThenTrue() throws UnknownHostException {
+		ServerWebExchange ipv4Exchange = exchange("192.168.1.104");
+		IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.0/24");
+		assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isTrue();
+	}
+
+	@Test
+	public void matchesWhenIpv4SubnetAndIpv4AddressThenFalse() throws UnknownHostException {
+		ServerWebExchange ipv4Exchange = exchange("192.168.1.104");
+		IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.128/25");
+		assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isFalse();
+	}
+
+	@Test
+	public void matchesWhenIpv6SubnetAndIpv6AddressThenTrue() throws UnknownHostException {
+		ServerWebExchange ipv6Exchange = exchange("2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF");
+		IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48");
+		assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isTrue();
+	}
+
+	@Test
+	public void matchesWhenIpv6SubnetAndIpv6AddressThenFalse() throws UnknownHostException {
+		ServerWebExchange ipv6Exchange = exchange("2001:DB8:1:0:0:0:0:0");
+		IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48");
+		assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isFalse();
+	}
+
+	@Test
+	public void matchesWhenZeroMaskAndAnythingThenTrue() throws UnknownHostException {
+		IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("0.0.0.0/0");
+		assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue();
+		assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue();
+		matcher = new IpAddressServerWebExchangeMatcher("192.168.0.159/0");
+		assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue();
+		assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue();
+	}
+
+	@Test
+	public void constructorWhenIpv4AddressMaskTooLongThenIllegalArgumentException() {
+		String ipv4AddressWithTooLongMask = "192.168.1.104/33";
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv4AddressWithTooLongMask))
+				.withMessage(String.format("IP address %s is too short for bitmask of length %d", "192.168.1.104", 33));
+	}
+
+	@Test
+	public void constructorWhenIpv6AddressMaskTooLongThenIllegalArgumentException() {
+		String ipv6AddressWithTooLongMask = "fe80::21f:5bff:fe33:bd68/129";
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv6AddressWithTooLongMask))
+				.withMessage(String.format("IP address %s is too short for bitmask of length %d",
+						"fe80::21f:5bff:fe33:bd68", 129));
+	}
+
+	private static ServerWebExchange exchange(String ipAddress) throws UnknownHostException {
+		return MockServerWebExchange.builder(MockServerHttpRequest.get("/")
+				.remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build();
+	}
+
+}