diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-07-21 14:51:32 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-08-17 17:04:19 +0200 |
commit | 9cd0d218e9528c24cf5ac2a521e37211aa6aeb1f (patch) | |
tree | 04f6ec6dc81386b0fb1569ca0eef105df7337862 /config-model | |
parent | ead8d2122f052436f5d38771df5db68483e10989 (diff) |
Restrict uri bindings for hosted applications
Diffstat (limited to 'config-model')
4 files changed, 186 insertions, 0 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/UriBindingsValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/UriBindingsValidator.java new file mode 100644 index 00000000000..6ff397d38ea --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/UriBindingsValidator.java @@ -0,0 +1,79 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.application.validation; + +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.vespa.model.VespaModel; +import com.yahoo.vespa.model.container.ApplicationContainerCluster; +import com.yahoo.vespa.model.container.component.BindingPattern; +import com.yahoo.vespa.model.container.component.Handler; +import com.yahoo.vespa.model.container.http.FilterBinding; +import com.yahoo.vespa.model.container.http.Http; + +import java.util.logging.Level; + +import static com.yahoo.config.model.ConfigModelContext.ApplicationType.HOSTED_INFRASTRUCTURE; + +/** + * Validates URI bindings for filters and handlers + * + * @author bjorncs + */ +class UriBindingsValidator extends Validator { + + @Override + public void validate(VespaModel model, DeployState deployState) { + for (ApplicationContainerCluster cluster : model.getContainerClusters().values()) { + for (Handler<?> handler : cluster.getHandlers()) { + for (BindingPattern binding : handler.getServerBindings()) { + validateUserBinding(binding, model, deployState); + } + } + Http http = cluster.getHttp(); + if (http != null) { + for (FilterBinding binding : cluster.getHttp().getBindings()) { + validateUserBinding(binding.binding(), model, deployState); + } + } + } + } + + private static void validateUserBinding(BindingPattern binding, VespaModel model, DeployState deployState) { + validateScheme(binding, deployState); + if (isHostedApplication(model, deployState)) { + validateHostedApplicationUserBinding(binding); + } + } + + private static void validateScheme(BindingPattern binding, DeployState deployState) { + if (binding.scheme().equals("https")) { + String message = createErrorMessage( + binding, "'https' bindings are deprecated, use 'http' instead to bind to both http and https traffic."); + deployState.getDeployLogger().log(Level.WARNING, message); + } + } + + private static void validateHostedApplicationUserBinding(BindingPattern binding) { + // only perform these validation for used-generated bindings + // bindings produced by the hosted config model amender will violate some of the rules below + if (!binding.isUserGenerated()) return; + + if (binding.port().isPresent()) { + throw new IllegalArgumentException(createErrorMessage(binding, "binding with port is not allowed")); + } + if (!binding.host().equals(BindingPattern.WILDCARD_PATTERN)) { + throw new IllegalArgumentException(createErrorMessage(binding, "only binding with wildcard ('*') for hostname is allowed")); + } + if (!binding.scheme().equals("http")) { + throw new IllegalArgumentException(createErrorMessage(binding, "only 'http' is allowed as scheme")); + } + } + + private static boolean isHostedApplication(VespaModel model, DeployState deployState) { + return deployState.isHosted() && model.getAdmin().getApplicationType() != HOSTED_INFRASTRUCTURE; + } + + private static String createErrorMessage(BindingPattern binding, String message) { + return String.format("For binding '%s': %s", binding.patternString(), message); + } + +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java index 22dd0289390..3a4dee300da 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java @@ -61,6 +61,7 @@ public class Validation { new AccessControlFilterValidator().validate(model, deployState); new CloudWatchValidator().validate(model, deployState); new AwsAccessControlValidator().validate(model, deployState); + new UriBindingsValidator().validate(model, deployState); List<ConfigChangeAction> result = Collections.emptyList(); if (deployState.getProperties().isFirstTimeDeployment()) { diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/BindingPattern.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/BindingPattern.java index 835dca722ac..ae524d0f06e 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/BindingPattern.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/BindingPattern.java @@ -21,6 +21,8 @@ public class BindingPattern implements Comparable<BindingPattern> { private static final Pattern BINDING_PATTERN = Pattern.compile("([^:]+)://([^:/]+)(:((\\*)|([0-9]+)))?(/.*)", Pattern.UNICODE_CASE | Pattern.CANON_EQ); + public static final String WILDCARD_PATTERN = "*"; + private final String scheme; private final String host; private final String port; diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/UriBindingsValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/UriBindingsValidatorTest.java new file mode 100644 index 00000000000..8e8e590d060 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/UriBindingsValidatorTest.java @@ -0,0 +1,104 @@ +package com.yahoo.vespa.model.application.validation;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.deploy.TestProperties; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.vespa.model.VespaModel; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xml.sax.SAXException; + +import java.io.IOException; + +/** + * @author bjorncs + */ +public class UriBindingsValidatorTest { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void fails_on_user_handler_binding_with_port() throws IOException, SAXException { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("For binding 'http://*:4443/my-handler': binding with port is not allowed"); + runUriBindingValidator(true, createServicesXmlWithHandler("http://*:4443/my-handler")); + } + + @Test + public void fails_on_user_handler_binding_with_hostname() throws IOException, SAXException { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("For binding 'http://myhostname/my-handler': only binding with wildcard ('*') for hostname is allowed"); + runUriBindingValidator(true, createServicesXmlWithHandler("http://myhostname/my-handler")); + } + + @Test + public void fails_on_user_handler_binding_with_non_http_scheme() throws IOException, SAXException { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("For binding 'https://*/my-handler': only 'http' is allowed as scheme"); + runUriBindingValidator(true, createServicesXmlWithHandler("https://*/my-handler")); + } + + @Test + public void fails_on_invalid_filter_binding() throws IOException, SAXException { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("For binding 'https://*:4443/my-request-filer-chain': binding with port is not allowed"); + runUriBindingValidator(true, createServicesXmlWithRequestFilterChain("https://*:4443/my-request-filer-chain")); + } + + @Test + public void allows_valid_user_binding() throws IOException, SAXException { + runUriBindingValidator(true, createServicesXmlWithHandler("http://*/my-handler")); + } + + @Test + public void only_restricts_user_bindings_on_hosted() throws IOException, SAXException { + runUriBindingValidator(false, createServicesXmlWithRequestFilterChain("https://*:4443/my-request-filer-chain")); + } + + private void runUriBindingValidator(boolean isHosted, String servicesXml) throws IOException, SAXException { + ApplicationPackage app = new MockApplicationPackage.Builder() + .withServices(servicesXml) + .build(); + DeployState deployState = new DeployState.Builder() + .applicationPackage(app) + .properties(new TestProperties().setHostedVespa(isHosted)) + .build(); + VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState); + new UriBindingsValidator().validate(model, deployState); + } + + private static String createServicesXmlWithHandler(String handlerBinding) { + return String.join( + "\n", + "<services version='1.0'>", + " <container id='default' version='1.0'>", + " <handler id='custom.Handler'>", + " <binding>" + handlerBinding + "</binding>", + " </handler>", + " </container>", + "</services>"); + } + + private static String createServicesXmlWithRequestFilterChain(String filterBinding) { + return String.join( + "\n", + "<services version='1.0'>", + " <container version='1.0'>", + " <http>", + " <server port='8080' id='main' />", + " <filtering>", + " <request-chain id='myChain'>", + " <filter id='myFilter'/>", + " <binding>" + filterBinding + "</binding>", + " </request-chain>", + " </filtering>", + " </http>", + " </container>", + "</services>"); + } + +}
\ No newline at end of file |