From 9cd0d218e9528c24cf5ac2a521e37211aa6aeb1f Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Tue, 21 Jul 2020 14:51:32 +0200 Subject: Restrict uri bindings for hosted applications --- .../validation/UriBindingsValidator.java | 79 ++++++++++++++++ .../model/application/validation/Validation.java | 1 + .../model/container/component/BindingPattern.java | 2 + .../validation/UriBindingsValidatorTest.java | 104 +++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 config-model/src/main/java/com/yahoo/vespa/model/application/validation/UriBindingsValidator.java create mode 100644 config-model/src/test/java/com/yahoo/vespa/model/application/validation/UriBindingsValidatorTest.java (limited to 'config-model') 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 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 { 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", + "", + " ", + " ", + " " + handlerBinding + "", + " ", + " ", + ""); + } + + private static String createServicesXmlWithRequestFilterChain(String filterBinding) { + return String.join( + "\n", + "", + " ", + " ", + " ", + " ", + " ", + " ", + " " + filterBinding + "", + " ", + " ", + " ", + " ", + ""); + } + +} \ No newline at end of file -- cgit v1.2.3