Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CIDR ranges in ignore_hosts setting. #5099

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ dependencies {
implementation 'com.nimbusds:nimbus-jose-jwt:9.48'
implementation 'com.rfksystems:blake2b:2.0.0'
implementation 'com.password4j:password4j:1.8.2'
implementation "commons-net:commons-net:3.11.1"

// Action privileges: check tables and compact collections
implementation 'com.selectivem.collections:special-collections-complete:1.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@
package org.opensearch.security.auth;

import java.net.InetAddress;
import java.util.List;
import java.util.Map;

import org.apache.commons.net.util.SubnetUtils;

import org.opensearch.security.support.WildcardMatcher;
import org.opensearch.security.user.AuthCredentials;

public interface AuthFailureListener {
List<String> getIgnoreHosts();
shikharj05 marked this conversation as resolved.
Show resolved Hide resolved

void onAuthFailure(InetAddress remoteAddress, AuthCredentials authCredentials, Object request);

WildcardMatcher getIgnoreHostsMatcher();

Map<String, SubnetUtils.SubnetInfo> getSubnetUtilsMatcherMap();
}
30 changes: 13 additions & 17 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
Expand Down Expand Up @@ -76,11 +75,13 @@
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import static org.opensearch.security.support.SecurityUtils.matchesHostNamePatterns;
import static org.opensearch.security.support.SecurityUtils.matchesIpAndCidrPatterns;
import static com.amazon.dlic.auth.http.saml.HTTPSamlAuthenticator.SAML_TYPE;

public class BackendRegistry {

protected final Logger log = LogManager.getLogger(this.getClass());
protected static final Logger log = LogManager.getLogger(BackendRegistry.class);
private SortedSet<AuthDomain> restAuthDomains;
private Set<AuthorizationBackend> restAuthorizers;

Expand Down Expand Up @@ -696,8 +697,7 @@ private boolean isBlocked(InetAddress address) {
}

for (ClientBlockRegistry<InetAddress> clientBlockRegistry : ipClientBlockRegistries) {
WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher();
if (matchesHostPatterns(ignoreHostsMatcher, address, hostResolverMode)) {
if (matchesIgnoreHostPatterns(clientBlockRegistry, address, hostResolverMode)) {
return false;
}
if (clientBlockRegistry.isBlocked(address)) {
Expand All @@ -708,21 +708,17 @@ private boolean isBlocked(InetAddress address) {
return false;
}

public static boolean matchesHostPatterns(WildcardMatcher hostMatcher, InetAddress address, String hostResolverMode) {
if (hostMatcher == null) {
private static boolean matchesIgnoreHostPatterns(
ClientBlockRegistry<InetAddress> clientBlockRegistry,
InetAddress address,
String hostResolverMode
) {
WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher();
if (ignoreHostsMatcher == null || address == null) {
return false;
}
if (address != null) {
List<String> valuesToCheck = new ArrayList<>(List.of(address.getHostAddress()));
if (hostResolverMode != null
&& (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) {
final String hostName = address.getHostName();
valuesToCheck.add(hostName);
}

return valuesToCheck.stream().anyMatch(hostMatcher);
}
return false;
return matchesHostNamePatterns(ignoreHostsMatcher, address, hostResolverMode)
|| matchesIpAndCidrPatterns(clientBlockRegistry, address);
}

private boolean isBlocked(String authBackend, String userName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,25 @@
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.net.util.SubnetUtils;

import org.opensearch.common.settings.Settings;
import org.opensearch.security.auth.AuthFailureListener;
import org.opensearch.security.auth.blocking.ClientBlockRegistry;
import org.opensearch.security.auth.blocking.HeapBasedClientBlockRegistry;
import org.opensearch.security.support.WildcardMatcher;
import org.opensearch.security.user.AuthCredentials;
import org.opensearch.security.util.IPAddressUtils;
import org.opensearch.security.util.ratetracking.RateTracker;

public abstract class AbstractRateLimiter<ClientIdType> implements AuthFailureListener, ClientBlockRegistry<ClientIdType> {
protected final ClientBlockRegistry<ClientIdType> clientBlockRegistry;
protected final RateTracker<ClientIdType> rateTracker;
protected final List<String> ignoreHosts;
private WildcardMatcher ignoreHostMatcher;
protected final Map<String, SubnetUtils.SubnetInfo> subnetUtilsMatcherMap;

public AbstractRateLimiter(Settings settings, Path configPath, Class<ClientIdType> clientIdType) {
this.ignoreHosts = settings.getAsList("ignore_hosts", Collections.emptyList());
Expand All @@ -48,6 +53,19 @@ public AbstractRateLimiter(Settings settings, Path configPath, Class<ClientIdTyp
settings.getAsInt("allowed_tries", 10),
settings.getAsInt("max_tracked_clients", 100_000)
);
this.subnetUtilsMatcherMap = IPAddressUtils.createSubnetUtils(
ignoreHosts.stream().filter(IPAddressUtils::isValidIpv4Cidr).toList()
);
}

@Override
public List<String> getIgnoreHosts() {
return ignoreHosts;
}

@Override
public Map<String, SubnetUtils.SubnetInfo> getSubnetUtilsMatcherMap() {
return subnetUtilsMatcherMap;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@

package org.opensearch.security.support;

import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.net.util.SubnetUtils;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.common.settings.Settings;
import org.opensearch.security.auth.AuthFailureListener;
import org.opensearch.security.auth.blocking.ClientBlockRegistry;
import org.opensearch.security.tools.Hasher;

public final class SecurityUtils {
Expand All @@ -46,6 +55,7 @@
static final Pattern ENVBC_PATTERN = Pattern.compile("\\$\\{envbc" + ENV_PATTERN_SUFFIX);
static final Pattern ENVBASE64_PATTERN = Pattern.compile("\\$\\{envbase64" + ENV_PATTERN_SUFFIX);
public static Locale EN_Locale = forEN();
private static final Map<String, SubnetInfo> cidrCache = new ConcurrentHashMap<>();
shikharj05 marked this conversation as resolved.
Show resolved Hide resolved

private SecurityUtils() {}

Expand Down Expand Up @@ -140,4 +150,53 @@
return bc ? Hasher.hash(envVarValue.toCharArray(), settings) : envVarValue;
}
}

// This is used to match host names - for e.g. *.example.local to dev.example.local
public static boolean matchesHostNamePatterns(WildcardMatcher hostMatcher, InetAddress address, String hostResolverMode) {
if (address == null || hostMatcher == null) {
return false;
}

// Only proceed with hostname checks if hostResolverMode is configured
if (hostResolverMode != null
&& (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) {
try {
List<String> valuesToCheck = new ArrayList<>(List.of(address.getHostAddress()));
final String hostName = address.getHostName();
valuesToCheck.add(hostName);
return valuesToCheck.stream().anyMatch(hostMatcher);
} catch (Exception e) {
log.warn("Failed to resolve hostname for {}: {}", address.getHostAddress(), e.getMessage());
return false;

Check warning on line 170 in src/main/java/org/opensearch/security/support/SecurityUtils.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/support/SecurityUtils.java#L168-L170

Added lines #L168 - L170 were not covered by tests
}
}
return false;
}

// This is used to match IP addresses - for e.g. 192.168.0.1 to 192.168.0.0/16
public static boolean matchesIpAndCidrPatterns(ClientBlockRegistry<InetAddress> clientBlockRegistry, InetAddress address) {
if (address == null || clientBlockRegistry == null) {
return false;
}

AuthFailureListener authFailureListener = (AuthFailureListener) clientBlockRegistry;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: As this method only works for AuthFailureListener instances, it should be probably an instance method of AuthFailureListener.

Alternatively: Have you considered to create a new class that encapsulates a combination of WildcardMatcher and CIDR matching?

This combination of matching is something what is not only useful for the ignore_hosts setting, but for quite a few functionalities. Having a reusable component for that would be nice.

final String hostAddress = address.getHostAddress();

for (String pattern : authFailureListener.getIgnoreHosts()) {
// Handle CIDR patterns - SubnetUtilsMatcherMap holds a SubnetInfo object for valid IPv4 patterns
if (authFailureListener.getSubnetUtilsMatcherMap().containsKey(pattern)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about directly iterating through the getSubnetUtilsMatcherMap().values()? Actually, then you'd not need a Map, but just a Collection, which would simplify the implementation.

SubnetUtils.SubnetInfo subnetInfo = authFailureListener.getSubnetUtilsMatcherMap().get(pattern);
if (subnetInfo != null && subnetInfo.isInRange(hostAddress)) {
return true;
}
} else {
// Handle both direct IPs and wildcard IP patterns
if (authFailureListener.getIgnoreHostsMatcher().test(hostAddress)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the loop does not use the looping variable pattern. Can't we move it out of the loop then?

return true;
}
}
}
return false;

}
}
79 changes: 79 additions & 0 deletions src/main/java/org/opensearch/security/util/IPAddressUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.net.util.SubnetUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.security.support.SecurityUtils;

public class IPAddressUtils {

Check warning on line 28 in src/main/java/org/opensearch/security/util/IPAddressUtils.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/util/IPAddressUtils.java#L28

Added line #L28 was not covered by tests

protected final static Logger log = LogManager.getLogger(SecurityUtils.class);
private static final String IP_ADDRESS = "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})";
private static final String SLASH_FORMAT = IP_ADDRESS + "/(\\d{1,2})"; // 0 -> 32
private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile(SLASH_FORMAT);

/**
* Creates a map of CIDR strings to their corresponding SubnetInfo objects
*
* @param hosts List of CIDR patterns to process
* @return Map of valid CIDR strings to their SubnetInfo objects
*/
public static Map<String, SubnetUtils.SubnetInfo> createSubnetUtils(List<String> hosts) {
if (hosts == null || hosts.isEmpty()) {
return Collections.emptyMap();
}

return hosts.stream()
.filter(Objects::nonNull)
.collect(
Collectors.toMap(
cidr -> cidr,
IPAddressUtils::getSubnetForCidr,
(existing, replacement) -> existing,

Check warning on line 52 in src/main/java/org/opensearch/security/util/IPAddressUtils.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/util/IPAddressUtils.java#L52

Added line #L52 was not covered by tests
() -> new HashMap<>(hosts.size())
)
);
}

/**
* Creates a SubnetInfo object for a given CIDR pattern
*
* @param cidr CIDR pattern to process
* @return SubnetInfo object for the given CIDR
*/
private static SubnetUtils.SubnetInfo getSubnetForCidr(String cidr) {
SubnetUtils utils = new SubnetUtils(cidr);
utils.setInclusiveHostCount(true);
return utils.getInfo();
}

/**
* Validates if a given string matches IPv4 CIDR pattern
*
* @param cidr String to validate
* @return true if the string is a valid IPv4 CIDR pattern, false otherwise
*/
public static boolean isValidIpv4Cidr(String cidr) {
return cidr != null && IPV4_CIDR_PATTERN.matcher(cidr).matches();
}
}
Loading
Loading