summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2020-07-21 14:51:32 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2020-08-17 17:04:19 +0200
commit9cd0d218e9528c24cf5ac2a521e37211aa6aeb1f (patch)
tree04f6ec6dc81386b0fb1569ca0eef105df7337862 /config-model
parentead8d2122f052436f5d38771df5db68483e10989 (diff)
Restrict uri bindings for hosted applications
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/UriBindingsValidator.java79
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java1
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/BindingPattern.java2
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/UriBindingsValidatorTest.java104
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