From 6a6e44545c038d2aee3460b0045b498871e3e095 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 2 Mar 2015 18:12:45 +0100 Subject: [PATCH] IP Filtering: Make IP filtering a dynamic setting In order to be able to configure ip filtering in a dynamic way, all the ip filter related settings have been made dynamic. This commit also fixed a bug, as the setting shield.http.filter.enabled was not working, but mentioned in the documentation. Documentation has been updated along the way. Closes elastic/elasticsearch#697 Original commit: elastic/x-pack-elasticsearch@2760c47b5bde01abbf02107932c03ab3a6e4d13b --- .../elasticsearch/shield/ShieldPlugin.java | 8 +- .../shield/transport/filter/IPFilter.java | 224 +++++++++++++++--- .../transport/filter/IPFilterTests.java | 56 ++++- .../filter/IpFilteringUpdateTests.java | 122 ++++++++++ .../shield/transport/filter/ip_filtering | 4 - .../IPFilterNettyUpstreamHandlerTests.java | 33 ++- 6 files changed, 399 insertions(+), 48 deletions(-) create mode 100644 src/test/java/org/elasticsearch/shield/transport/filter/IpFilteringUpdateTests.java delete mode 100644 src/test/java/org/elasticsearch/shield/transport/filter/ip_filtering diff --git a/src/main/java/org/elasticsearch/shield/ShieldPlugin.java b/src/main/java/org/elasticsearch/shield/ShieldPlugin.java index 8ccfc98eef9..7734b54ef3d 100644 --- a/src/main/java/org/elasticsearch/shield/ShieldPlugin.java +++ b/src/main/java/org/elasticsearch/shield/ShieldPlugin.java @@ -7,6 +7,7 @@ package org.elasticsearch.shield; import org.elasticsearch.client.Client; import org.elasticsearch.client.support.Headers; +import org.elasticsearch.cluster.settings.ClusterDynamicSettingsModule; import org.elasticsearch.common.collect.ImmutableList; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Module; @@ -20,6 +21,7 @@ import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authz.store.FileRolesStore; import org.elasticsearch.shield.license.LicenseService; import org.elasticsearch.shield.signature.InternalSignatureService; +import org.elasticsearch.shield.transport.filter.IPFilter; import java.io.File; import java.nio.file.Path; @@ -65,7 +67,7 @@ public class ShieldPlugin extends AbstractPlugin { @Override public Collection> services() { return enabled && !clientMode ? - ImmutableList.>of(LicenseService.class, FileRolesStore.class, Realms.class, InternalSignatureService.class) : + ImmutableList.>of(LicenseService.class, FileRolesStore.class, Realms.class, InternalSignatureService.class, IPFilter.class) : ImmutableList.>of(); } @@ -81,6 +83,10 @@ public class ShieldPlugin extends AbstractPlugin { return settingsBuilder.build(); } + public void onModule(ClusterDynamicSettingsModule clusterDynamicSettingsModule) { + clusterDynamicSettingsModule.addDynamicSettings("shield.transport.filter.*", "shield.http.filter.*", "transport.profiles.*", IPFilter.IP_FILTER_ENABLED_SETTING, IPFilter.IP_FILTER_ENABLED_HTTP_SETTING); + } + private void addUserSettings(ImmutableSettings.Builder settingsBuilder) { String authHeaderSettingName = Headers.PREFIX + "." + UsernamePasswordToken.BASIC_AUTH_HEADER; if (settings.get(authHeaderSettingName) != null) { diff --git a/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java b/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java index 5bd5fa4f8e3..e670c292900 100644 --- a/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java +++ b/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java @@ -5,24 +5,29 @@ */ package org.elasticsearch.shield.transport.filter; -import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.collect.HppcMaps; import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.collect.Maps; import org.elasticsearch.common.collect.ObjectArrays; -import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.hppc.ObjectObjectOpenHashMap; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.jackson.dataformat.yaml.snakeyaml.error.YAMLException; -import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; +import org.elasticsearch.transport.Transport; -import java.io.IOException; import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.Map; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; -public class IPFilter extends AbstractComponent { +public class IPFilter extends AbstractLifecycleComponent { /** * .http has been chosen for handling HTTP filters, which are not part of the profiles @@ -32,6 +37,9 @@ public class IPFilter extends AbstractComponent { */ public static final String HTTP_PROFILE_NAME = ".http"; + public static final String IP_FILTER_ENABLED_SETTING = "shield.transport.filter.enabled"; + public static final String IP_FILTER_ENABLED_HTTP_SETTING = "shield.http.filter.enabled"; + public static final ShieldIpFilterRule DEFAULT_PROFILE_ACCEPT_ALL = new ShieldIpFilterRule(true, "default:accept_all") { @Override public boolean contains(InetAddress inetAddress) { @@ -49,15 +57,61 @@ public class IPFilter extends AbstractComponent { } }; + private final LifecycleListener parseSettingsListener = new LifecycleListener() { + @Override + public void afterStart() { + IPFilter.this.rules = IPFilter.this.parseSettings(settings); + } + }; + + private NodeSettingsService nodeSettingsService; private final AuditTrail auditTrail; - private final Map rules; + private final Transport transport; + private Map rules = Collections.EMPTY_MAP; + private HttpServerTransport httpServerTransport = null; @Inject - public IPFilter(Settings settings, AuditTrail auditTrail) { + public IPFilter(final Settings settings, AuditTrail auditTrail, NodeSettingsService nodeSettingsService, Transport transport) { super(settings); + this.nodeSettingsService = nodeSettingsService; this.auditTrail = auditTrail; - rules = parseSettings(settings, logger); + this.transport = transport; + } + + @Override + protected void doStart() throws ElasticsearchException { + nodeSettingsService.addListener(new ApplySettings(settings)); + + if (transport.lifecycleState() == Lifecycle.State.STARTED) { + rules = parseSettings(settings); + } else { + transport.addLifecycleListener(parseSettingsListener); + } + } + + @Override + protected void doStop() throws ElasticsearchException { + } + + @Override + protected void doClose() throws ElasticsearchException { + } + + // this cannot be put into the constructor as HTTP might be disabled + @Inject(optional = true) + public void setHttpServerTransport(@Nullable HttpServerTransport httpServerTransport) { + if (httpServerTransport == null) { + return; + } + + this.httpServerTransport = httpServerTransport; + + if (httpServerTransport.lifecycleState() == Lifecycle.State.STARTED) { + IPFilter.this.rules = IPFilter.this.parseSettings(settings); + } else { + httpServerTransport.addLifecycleListener(parseSettingsListener); + } } public boolean accept(String profile, InetAddress peerAddress) { @@ -81,44 +135,154 @@ public class IPFilter extends AbstractComponent { return true; } - private static Map parseSettings(Settings settings, ESLogger logger) { - if (!settings.getAsBoolean("shield.transport.filter.enabled", true)) { + private Map parseSettings(Settings settings) { + boolean isIpFilterEnabled = settings.getAsBoolean(IP_FILTER_ENABLED_SETTING, true); + boolean isHttpFilterEnabled = settings.getAsBoolean(IP_FILTER_ENABLED_HTTP_SETTING, isIpFilterEnabled); + + if (!isIpFilterEnabled && !isHttpFilterEnabled) { return Collections.EMPTY_MAP; } Map profileRules = Maps.newHashMap(); - String[] allowed = settings.getAsArray("shield.transport.filter.allow"); - String[] denied = settings.getAsArray("shield.transport.filter.deny"); - String[] httpAllowed = settings.getAsArray("shield.http.filter.allow", settings.getAsArray("transport.profiles.default.shield.filter.allow", settings.getAsArray("shield.transport.filter.allow"))); - String[] httpDdenied = settings.getAsArray("shield.http.filter.deny", settings.getAsArray("transport.profiles.default.shield.filter.deny", settings.getAsArray("shield.transport.filter.deny"))); - try { - profileRules.put("default", ObjectArrays.concat(parseValue(allowed, true), parseValue(denied, false), ShieldIpFilterRule.class)); - profileRules.put(HTTP_PROFILE_NAME, ObjectArrays.concat(parseValue(httpAllowed, true), parseValue(httpDdenied, false), ShieldIpFilterRule.class)); + if (isHttpFilterEnabled && httpServerTransport != null && httpServerTransport.lifecycleState() == Lifecycle.State.STARTED) { + InetAddress localAddress = ((InetSocketTransportAddress) this.httpServerTransport.boundAddress().boundAddress()).address().getAddress(); + String[] httpAllowed = settings.getAsArray("shield.http.filter.allow", settings.getAsArray("transport.profiles.default.shield.filter.allow", settings.getAsArray("shield.transport.filter.allow"))); + String[] httpDdenied = settings.getAsArray("shield.http.filter.deny", settings.getAsArray("transport.profiles.default.shield.filter.deny", settings.getAsArray("shield.transport.filter.deny"))); + profileRules.put(HTTP_PROFILE_NAME, ObjectArrays.concat(parseValue(httpAllowed, true, localAddress), parseValue(httpDdenied, false, localAddress), ShieldIpFilterRule.class)); + } + + if (isIpFilterEnabled && this.transport.lifecycleState() == Lifecycle.State.STARTED) { + InetAddress localAddress = ((InetSocketTransportAddress) this.transport.boundAddress().boundAddress()).address().getAddress(); + + String[] allowed = settings.getAsArray("shield.transport.filter.allow"); + String[] denied = settings.getAsArray("shield.transport.filter.deny"); + profileRules.put("default", ObjectArrays.concat(parseValue(allowed, true, localAddress), parseValue(denied, false, localAddress), ShieldIpFilterRule.class)); Map groupedSettings = settings.getGroups("transport.profiles."); for (Map.Entry entry : groupedSettings.entrySet()) { String profile = entry.getKey(); Settings profileSettings = entry.getValue().getByPrefix("shield.filter."); profileRules.put(profile, ObjectArrays.concat( - parseValue(profileSettings.getAsArray("allow"), true), - parseValue(profileSettings.getAsArray("deny"), false), + parseValue(profileSettings.getAsArray("allow"), true, localAddress), + parseValue(profileSettings.getAsArray("deny"), false, localAddress), ShieldIpFilterRule.class)); } - - } catch (IOException | YAMLException e) { - throw new ElasticsearchParseException("failed to read & parse rules from settings", e); } logger.debug("loaded ip filtering profiles: {}", profileRules.keySet()); return ImmutableMap.copyOf(profileRules); } - private static ShieldIpFilterRule[] parseValue(String[] values, boolean isAllowRule) throws UnknownHostException { - ShieldIpFilterRule[] rules = new ShieldIpFilterRule[values.length]; + private ShieldIpFilterRule[] parseValue(String[] values, boolean isAllowRule, InetAddress localAddress) { + List rules = new ArrayList<>(); for (int i = 0; i < values.length; i++) { - rules[i] = new ShieldIpFilterRule(isAllowRule, values[i]); + + // never ever deny on localhost, do not even add this rule + if (!isAllowRule && isLocalAddress(localAddress, values[i])) { + logger.warn("Configuration setting not applied to reject connections on [{}]. local connections are always allowed!", values[i]); + continue; + } + + rules.add(new ShieldIpFilterRule(isAllowRule, values[i])); + } + return rules.toArray(new ShieldIpFilterRule[]{}); + } + + private boolean isLocalAddress(InetAddress localAddress, String address) { + return address.equals("127.0.0.1") || address.equals("localhost") || address.equals("::1") || address.startsWith("fe80::1") || + address.equals(localAddress.getHostAddress()) || address.equals(localAddress.getHostName()); + } + + private class ApplySettings implements NodeSettingsService.Listener { + + String[] allowed; + String[] denied; + String[] httpAllowed; + String[] httpDenied; + ObjectObjectOpenHashMap profileAllowed; + ObjectObjectOpenHashMap profileDenied; + private boolean enabled; + private boolean httpEnabled; + + public ApplySettings(Settings settings) { + loadValuesFromSettings(settings); + } + + private void loadValuesFromSettings(Settings settings) { + this.enabled = settings.getAsBoolean(IP_FILTER_ENABLED_SETTING, this.enabled); + this.httpEnabled = settings.getAsBoolean(IP_FILTER_ENABLED_HTTP_SETTING, this.httpEnabled); + this.allowed = settings.getAsArray("shield.transport.filter.allow"); + this.denied = settings.getAsArray("shield.transport.filter.deny"); + this.httpAllowed = settings.getAsArray("shield.http.filter.allow"); + this.httpDenied = settings.getAsArray("shield.http.filter.deny"); + + if (settings.getGroups("transport.profiles.").size() == 0) { + profileAllowed = HppcMaps.newMap(0); + profileDenied = HppcMaps.newMap(0); + } + + profileAllowed = HppcMaps.newNoNullKeysMap(settings.getGroups("transport.profiles.").size()); + profileDenied = HppcMaps.newNoNullKeysMap(settings.getGroups("transport.profiles.").size()); + for (Map.Entry entry : settings.getGroups("transport.profiles.").entrySet()) { + profileAllowed.put(entry.getKey(), entry.getValue().getAsArray("shield.filter.allow")); + profileDenied.put(entry.getKey(), entry.getValue().getAsArray("shield.filter.deny")); + } + } + + @Override + public void onRefreshSettings(Settings settings) { + if (ipFilterSettingsInvolved(settings) && settingsChanged(settings)) { + IPFilter.this.rules = parseSettings(settings); + loadValuesFromSettings(settings); + } + } + + private boolean settingsChanged(Settings settings) { + // simple checks first + if (this.enabled != settings.getAsBoolean(IP_FILTER_ENABLED_SETTING, this.enabled) || + this.httpEnabled != settings.getAsBoolean(IP_FILTER_ENABLED_HTTP_SETTING, this.httpEnabled) || + !Arrays.equals(allowed, settings.getAsArray("shield.transport.filter.allow")) || + !Arrays.equals(denied, settings.getAsArray("shield.transport.filter.deny")) || + !Arrays.equals(httpAllowed, settings.getAsArray("shield.http.filter.allow")) || + !Arrays.equals(httpDenied, settings.getAsArray("shield.http.filter.deny")) + ) { + return true; + } + + // profile checks now + ObjectObjectOpenHashMap newProfileAllowed = HppcMaps.newNoNullKeysMap(settings.getGroups("transport.profiles.").size()); + ObjectObjectOpenHashMap newProfileDenied = HppcMaps.newNoNullKeysMap(settings.getGroups("transport.profiles.").size()); + for (Map.Entry entry : settings.getGroups("transport.profiles.").entrySet()) { + newProfileAllowed.put(entry.getKey(), entry.getValue().getAsArray("shield.filter.allow")); + newProfileDenied.put(entry.getKey(), entry.getValue().getAsArray("shield.filter.deny")); + } + + boolean allowedProfileChanged = !newProfileAllowed.equals(profileAllowed); + boolean deniedProfileChanged = !newProfileDenied.equals(profileDenied); + return allowedProfileChanged || deniedProfileChanged; + } + + private boolean ipFilterSettingsInvolved(Settings settings) { + boolean containsStaticIpFilterSettings = settings.get("shield.transport.filter.allow") != null || + settings.get("shield.transport.filter.deny") != null || + settings.get("shield.http.filter.allow") != null || + settings.get("shield.http.filter.deny") != null || + settings.get(IP_FILTER_ENABLED_SETTING) != null || + settings.get(IP_FILTER_ENABLED_HTTP_SETTING) != null; + + if (containsStaticIpFilterSettings) { + return true; + } + + // now if any profile has a filter setting configured + for (Map.Entry entry : settings.getGroups("transport.profiles.").entrySet()) { + if (entry.getValue().get("shield.filter.allow") != null || entry.getValue().get("shield.filter.deny") != null) { + return true; + } + } + + return false; } - return rules; } } diff --git a/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java b/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java index c0c983a3f4c..a36176f1e23 100644 --- a/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java @@ -5,11 +5,18 @@ */ package org.elasticsearch.shield.transport.filter; +import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.net.InetAddresses; +import org.elasticsearch.common.network.NetworkUtils; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.test.junit.annotations.Network; +import org.elasticsearch.transport.Transport; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -28,10 +35,24 @@ public class IPFilterTests extends ElasticsearchTestCase { private IPFilter ipFilter; private AuditTrail auditTrail; + private Transport transport; + private HttpServerTransport httpTransport; + private NodeSettingsService nodeSettingsService; @Before public void init() { auditTrail = mock(AuditTrail.class); + nodeSettingsService = mock(NodeSettingsService.class); + + httpTransport = mock(HttpServerTransport.class); + InetSocketTransportAddress httpAddress = new InetSocketTransportAddress(NetworkUtils.getLocalAddress(), 9200); + when(httpTransport.boundAddress()).thenReturn(new BoundTransportAddress(httpAddress, httpAddress)); + when(httpTransport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); + + transport = mock(Transport.class); + InetSocketTransportAddress address = new InetSocketTransportAddress(NetworkUtils.getLocalAddress(), 9300); + when(transport.boundAddress()).thenReturn(new BoundTransportAddress(address, address)); + when(transport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); } @Test @@ -40,7 +61,7 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("10.2.3.4"); @@ -54,7 +75,7 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "2001:0db8:1234::/48") .putArray("shield.transport.filter.deny", "1234:db8:85a3:0:0:8a2e:370:7334", "4321:db8:1234::/48") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("2001:0db8:1234:0000:0000:8a2e:0370:7334"); assertAddressIsDenied("1234:0db8:85a3:0000:0000:8a2e:0370:7334"); @@ -68,7 +89,7 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "*.google.com") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("8.8.8.8"); @@ -79,7 +100,7 @@ public class IPFilterTests extends ElasticsearchTestCase { Settings settings = settingsBuilder() .put("shield.transport.filter.allow", "_all") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsAllowed("173.194.70.100"); @@ -93,7 +114,7 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("transport.profiles.client.shield.filter.allow", "192.168.0.1") .put("transport.profiles.client.shield.filter.deny", "_all") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("192.168.0.1"); @@ -107,7 +128,7 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "10.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("10.0.0.1"); assertAddressIsDenied("10.0.0.2"); @@ -116,7 +137,7 @@ public class IPFilterTests extends ElasticsearchTestCase { @Test public void testDefaultAllow() throws Exception { Settings settings = settingsBuilder().build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); assertAddressIsAllowed("10.0.0.1"); assertAddressIsAllowed("10.0.0.2"); @@ -128,12 +149,13 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .put("shield.http.filter.allow", "10.0.0.0/8") - .put("shield.http.filter.deny", "127.0.0.1") + .put("shield.http.filter.deny", "192.168.0.1") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter.setHttpServerTransport(httpTransport); assertAddressIsAllowedForProfile(IPFilter.HTTP_PROFILE_NAME, "10.2.3.4"); - assertAddressIsDeniedForProfile(IPFilter.HTTP_PROFILE_NAME, "127.0.0.1"); + assertAddressIsDeniedForProfile(IPFilter.HTTP_PROFILE_NAME, "192.168.0.1"); } @Test @@ -142,12 +164,24 @@ public class IPFilterTests extends ElasticsearchTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter.setHttpServerTransport(httpTransport); assertAddressIsAllowedForProfile(IPFilter.HTTP_PROFILE_NAME, "127.0.0.1"); assertAddressIsDeniedForProfile(IPFilter.HTTP_PROFILE_NAME, "10.2.3.4"); } + @Test + public void testThatLocalhostIsNeverRejected() throws Exception { + Settings settings = settingsBuilder() + .put("shield.transport.filter.deny", "127.0.0.1") + .build(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter.setHttpServerTransport(httpTransport); + + assertAddressIsAllowedForProfile(IPFilter.HTTP_PROFILE_NAME, "127.0.0.1"); + } + private void assertAddressIsAllowedForProfile(String profile, String ... inetAddresses) { for (String inetAddress : inetAddresses) { String message = String.format(Locale.ROOT, "Expected address %s to be allowed", inetAddress); diff --git a/src/test/java/org/elasticsearch/shield/transport/filter/IpFilteringUpdateTests.java b/src/test/java/org/elasticsearch/shield/transport/filter/IpFilteringUpdateTests.java new file mode 100644 index 00000000000..8aaf7c46af8 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/transport/filter/IpFilteringUpdateTests.java @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.transport.filter; + +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.internal.InternalNode; +import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; +import org.elasticsearch.test.ShieldIntegrationTest; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Locale; + +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.TEST; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.is; + +@ClusterScope(scope = TEST, numDataNodes = 1) +public class IpFilteringUpdateTests extends ShieldIntegrationTest { + + private boolean httpEnabled = false; + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + httpEnabled = randomBoolean(); + + return settingsBuilder() + .put(super.nodeSettings(nodeOrdinal)) + .put(InternalNode.HTTP_ENABLED, httpEnabled) + .put("shield.transport.filter.deny", "127.0.0.200") + .build(); + } + + @Test + public void testThatIpFilterConfigurationCanBeChangedDynamically() throws Exception { + // ensure this did not get overwritten by the listener during startup + assertConnectionRejected("default", "127.0.0.200"); + + // allow all by default + assertConnectionAccepted("default", "127.0.0.8"); + assertConnectionAccepted(".http", "127.0.0.8"); + assertConnectionAccepted("client", "127.0.0.8"); + + Settings settings = settingsBuilder() + .put("shield.transport.filter.allow", "127.0.0.1") + .put("shield.transport.filter.deny", "127.0.0.8") + .build(); + assertAcked(client().admin().cluster().prepareUpdateSettings().setTransientSettings(settings)); + assertConnectionRejected("default", "127.0.0.8"); + + settings = settingsBuilder() + .putArray("shield.http.filter.allow", "127.0.0.1") + .putArray("shield.http.filter.deny", "127.0.0.8") + .build(); + assertAcked(client().admin().cluster().prepareUpdateSettings().setPersistentSettings(settings)); + assertConnectionRejected("default", "127.0.0.8"); + assertConnectionRejected(".http", "127.0.0.8"); + + settings = settingsBuilder() + .put("transport.profiles.client.shield.filter.allow", "127.0.0.1") + .put("transport.profiles.client.shield.filter.deny", "127.0.0.8") + .build(); + assertAcked(client().admin().cluster().prepareUpdateSettings().setTransientSettings(settings)); + assertConnectionRejected("default", "127.0.0.8"); + assertConnectionRejected(".http", "127.0.0.8"); + assertConnectionRejected("client", "127.0.0.8"); + + // check that all is in cluster state + ClusterState clusterState = client().admin().cluster().prepareState().get().getState(); + assertThat(clusterState.metaData().settings().get("shield.transport.filter.allow"), is("127.0.0.1")); + assertThat(clusterState.metaData().settings().get("shield.transport.filter.deny"), is("127.0.0.8")); + assertThat(clusterState.metaData().persistentSettings().get("shield.http.filter.allow.0"), is("127.0.0.1")); + assertThat(clusterState.metaData().persistentSettings().get("shield.http.filter.deny.0"), is("127.0.0.8")); + assertThat(clusterState.metaData().settings().get("transport.profiles.client.shield.filter.allow"), is("127.0.0.1")); + assertThat(clusterState.metaData().settings().get("transport.profiles.client.shield.filter.deny"), is("127.0.0.8")); + + // now disable ip filtering dynamically and make sure nothing is rejected + settings = settingsBuilder() + .put(IPFilter.IP_FILTER_ENABLED_SETTING, false) + .put(IPFilter.IP_FILTER_ENABLED_HTTP_SETTING, true) + .build(); + assertAcked(client().admin().cluster().prepareUpdateSettings().setPersistentSettings(settings)); + assertConnectionAccepted("default", "127.0.0.8"); + assertConnectionAccepted("client", "127.0.0.8"); + + // now also disable for HTTP + assertConnectionRejected(".http", "127.0.0.8"); + settings = settingsBuilder() + .put(IPFilter.IP_FILTER_ENABLED_HTTP_SETTING, false) + .build(); + assertAcked(client().admin().cluster().prepareUpdateSettings().setPersistentSettings(settings)); + assertConnectionAccepted(".http", "127.0.0.8"); + } + + private void assertConnectionAccepted(String profile, String host) throws UnknownHostException { + // HTTP is not applied if disabled + if (!httpEnabled && IPFilter.HTTP_PROFILE_NAME.equals(profile)) { + return; + } + + IPFilter ipFilter = internalCluster().getInstance(IPFilter.class); + String message = String.format(Locale.ROOT, "Expected allowed connection for profile %s against host %s", profile, host); + assertThat(message, ipFilter.accept(profile, InetAddress.getByName(host)), is(true)); + } + + private void assertConnectionRejected(String profile, String host) throws UnknownHostException { + // HTTP is not applied if disabled + if (!httpEnabled && IPFilter.HTTP_PROFILE_NAME.equals(profile)) { + return; + } + + IPFilter ipFilter = internalCluster().getInstance(IPFilter.class); + String message = String.format(Locale.ROOT, "Expected rejection for profile %s against host %s", profile, host); + assertThat(message, ipFilter.accept(profile, InetAddress.getByName(host)), is(false)); + } +} diff --git a/src/test/java/org/elasticsearch/shield/transport/filter/ip_filtering b/src/test/java/org/elasticsearch/shield/transport/filter/ip_filtering deleted file mode 100644 index a2f8c6acee0..00000000000 --- a/src/test/java/org/elasticsearch/shield/transport/filter/ip_filtering +++ /dev/null @@ -1,4 +0,0 @@ -+c:255.255.0.0/16 --i:192.168.100.2 --c:2001:db8::/48 --n:hostname diff --git a/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java b/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java index 6b5ef53e176..522e24ff021 100644 --- a/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java +++ b/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java @@ -6,11 +6,18 @@ package org.elasticsearch.shield.transport.netty; import com.google.common.net.InetAddresses; +import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.netty.channel.*; +import org.elasticsearch.common.network.NetworkUtils; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.transport.filter.IPFilter; import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.transport.Transport; import org.junit.Before; import org.junit.Test; @@ -19,6 +26,8 @@ import java.net.SocketAddress; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @@ -34,9 +43,29 @@ public class IPFilterNettyUpstreamHandlerTests extends ElasticsearchTestCase { .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - IPFilter ipFilter = new IPFilter(settings, AuditTrail.NOOP); + boolean isHttpEnabled = randomBoolean(); - nettyUpstreamHandler = new IPFilterNettyUpstreamHandler(ipFilter, IPFilter.HTTP_PROFILE_NAME); + Transport transport = mock(Transport.class); + InetSocketTransportAddress address = new InetSocketTransportAddress(NetworkUtils.getLocalAddress(), 9300); + when(transport.boundAddress()).thenReturn(new BoundTransportAddress(address, address)); + when(transport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); + + NodeSettingsService nodeSettingsService = mock(NodeSettingsService.class); + IPFilter ipFilter = new IPFilter(settings, AuditTrail.NOOP, nodeSettingsService, transport).start(); + + if (isHttpEnabled) { + HttpServerTransport httpTransport = mock(HttpServerTransport.class); + InetSocketTransportAddress httpAddress = new InetSocketTransportAddress(NetworkUtils.getLocalAddress(), 9200); + when(httpTransport.boundAddress()).thenReturn(new BoundTransportAddress(httpAddress, httpAddress)); + when(httpTransport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); + ipFilter.setHttpServerTransport(httpTransport); + } + + if (isHttpEnabled) { + nettyUpstreamHandler = new IPFilterNettyUpstreamHandler(ipFilter, IPFilter.HTTP_PROFILE_NAME); + } else { + nettyUpstreamHandler = new IPFilterNettyUpstreamHandler(ipFilter, "default"); + } } @Test