summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorgjoranv <gv@oath.com>2018-03-14 15:48:01 +0100
committergjoranv <gv@oath.com>2018-03-19 12:13:33 +0100
commit6ae27a8d968d0399796c53f46ccfba607f70d977 (patch)
tree626a48a6cfc728f0d64d429f6ec1d938a18fcda7 /config-model
parentf11f286bf449a56f3618e8d8ad6d4a6be817b3bd (diff)
Require access-control write protection for first deployments.
- For hosted applications in prod zones. - Only applies to container clusters that have non-whitelisted handlers witn non-mbus server bindings. - Add general validation concept for first deployments.
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java6
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidator.java56
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/AccessControl.java6
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidatorTest.java123
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala10
5 files changed, 199 insertions, 2 deletions
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 490627e7b45..ff1e83273a2 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
@@ -15,6 +15,7 @@ import com.yahoo.vespa.model.application.validation.change.ContentClusterRemoval
import com.yahoo.vespa.model.application.validation.change.IndexedSearchClusterChangeValidator;
import com.yahoo.vespa.model.application.validation.change.IndexingModeChangeValidator;
import com.yahoo.vespa.model.application.validation.change.StartupCommandChangeValidator;
+import com.yahoo.vespa.model.application.validation.first.AccessControlValidator;
import java.time.Instant;
import java.util.ArrayList;
@@ -60,6 +61,7 @@ public class Validation {
return validateChanges((VespaModel)currentActiveModel.get(), model,
deployState.validationOverrides(), deployState.getDeployLogger(), deployState.now());
else
+ validateFirstTimeDeployment(model, deployState);
return new ArrayList<>();
}
@@ -80,4 +82,8 @@ public class Validation {
.collect(toList());
}
+ private static void validateFirstTimeDeployment(VespaModel model, DeployState deployState) {
+ new AccessControlValidator().validate(model, deployState);
+ }
+
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidator.java
new file mode 100644
index 00000000000..6e53634639c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidator.java
@@ -0,0 +1,56 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.first;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.Validator;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Handler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.yahoo.collections.CollectionUtil.mkString;
+import static com.yahoo.vespa.model.container.http.AccessControl.isBuiltinGetOnly;
+
+/**
+ * Validates that hosted applications in prod zones have write protection enabled.
+ *
+ * @author gjoranv
+ */
+public class AccessControlValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+
+ // TODO: what about zone application? (HOSTED_INFRASTRUCTURE)
+
+ if (deployState.isHosted() && deployState.zone().environment().isProduction()) {
+ List<String> offendingClusters = new ArrayList<>();
+ for (ContainerCluster cluster : model.getContainerClusters().values()) {
+ if (cluster.getHttp() == null
+ || ! cluster.getHttp().getAccessControl().isPresent()
+ || ! cluster.getHttp().getAccessControl().get().writeEnabled)
+
+ if (hasHandlerThatNeedsProtection(cluster) || ! cluster.getAllServlets().isEmpty())
+ offendingClusters.add(cluster.getName());
+ }
+ if (! offendingClusters.isEmpty())
+ throw new IllegalArgumentException(
+ "Access-control must be enabled for write operations to container clusters in production zones: " +
+ mkString(offendingClusters, "[", ", ", "]."));
+ }
+ }
+
+ private boolean hasHandlerThatNeedsProtection(ContainerCluster cluster) {
+ return cluster.getHandlers().stream().anyMatch(this::handlerNeedsProtection);
+ }
+
+ private boolean handlerNeedsProtection(Handler<?> handler) {
+ return ! isBuiltinGetOnly(handler) && hasNonMbusBinding(handler);
+ }
+
+ private boolean hasNonMbusBinding(Handler<?> handler) {
+ return handler.getServerBindings().stream().anyMatch(binding -> ! binding.startsWith("mbus"));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/AccessControl.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/AccessControl.java
index dc5f09bcfb6..0cea4a572b2 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/AccessControl.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/AccessControl.java
@@ -135,10 +135,14 @@ public final class AccessControl {
}
private boolean shouldHandlerBeProtected(Handler<?> handler) {
- return ! UNPROTECTED_HANDLERS.contains(handler.getClassId().getName())
+ return ! isBuiltinGetOnly(handler)
&& handler.getServerBindings().stream().noneMatch(excludedBindings::contains);
}
+ public static boolean isBuiltinGetOnly(Handler<?> handler) {
+ return UNPROTECTED_HANDLERS.contains(handler.getClassId().getName());
+ }
+
private boolean shouldServletBeProtected(Servlet servlet) {
return servletBindings(servlet).noneMatch(excludedBindings::contains);
}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidatorTest.java
new file mode 100644
index 00000000000..e6edb18c8da
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidatorTest.java
@@ -0,0 +1,123 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.first;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+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;
+
+import static com.yahoo.config.model.test.TestUtil.joinLines;
+import static com.yahoo.config.provision.Environment.prod;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author gjoranv
+ */
+public class AccessControlValidatorTest {
+
+ @Rule
+ public final ExpectedException exceptionRule = ExpectedException.none();
+
+ private static String servicesXml(boolean addHandler, boolean writeProtection) {
+ return joinLines("<services version='1.0'>",
+ " <container id='default' version='1.0'>",
+ addHandler ? httpHandlerXml : "",
+ " <http>",
+ " <filtering>",
+ " <access-control domain='foo' write='" + writeProtection + "' />",
+ " </filtering>",
+ " </http>",
+ " </container>",
+ "</services>");
+ }
+
+ private static final String httpHandlerXml =
+ joinLines(" <handler id='foo'>",
+ " <binding>http://foo/bar</binding>",
+ " </handler>");
+
+ @Test
+ public void cluster_with_write_protection_passes_validation() throws IOException, SAXException{
+ DeployState deployState = deployState(servicesXml(true, true));
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+ }
+
+ @Test
+ public void cluster_with_no_handlers_passes_validation_without_write_protection() throws IOException, SAXException{
+ DeployState deployState = deployState(servicesXml(false, false));
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+ }
+
+ @Test
+ public void cluster_without_custom_components_passes_validation_without_write_protection() throws IOException, SAXException{
+ String servicesXml = joinLines("<services version='1.0'>",
+ " <container id='default' version='1.0' />",
+ "</services>");
+ DeployState deployState = deployState(servicesXml);
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+ }
+
+ @Test
+ public void cluster_with_handler_fails_validation_without_write_protection() throws IOException, SAXException{
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "Access-control must be enabled for write operations to container clusters in production zones: [default]");
+
+ DeployState deployState = deployState(servicesXml(true, false));
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+
+ }
+
+ @Test
+ public void cluster_with_handler_fails_validation_without_http_element() throws IOException, SAXException{
+ exceptionRule.expect(IllegalArgumentException.class);
+ exceptionRule.expectMessage(
+ "Access-control must be enabled for write operations to container clusters in production zones: [default]");
+
+ String servicesXml = joinLines("<services version='1.0'>",
+ " <container id='default' version='1.0'>",
+ httpHandlerXml,
+ " </container>",
+ "</services>");
+ DeployState deployState = deployState(servicesXml);
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+ }
+
+ private static DeployState deployState(String servicesXml) {
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withServices(servicesXml)
+ .build();
+
+ DeployState.Builder builder = new DeployState.Builder()
+ .applicationPackage(app)
+ .properties(new DeployProperties.Builder()
+ .hostedVespa(true)
+ .build());
+ final DeployState deployState = builder.build(true);
+
+ assertTrue("Test must emulate a hosted deployment.", deployState.isHosted());
+ assertEquals("Test must emulate a prod environment.", prod, deployState.zone().environment());
+
+ return deployState;
+ }
+
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala
index 9847a76ecc4..4ebe14c1e85 100644
--- a/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala
@@ -22,8 +22,8 @@ class ImplicitIndexingClusterTest {
<jdisc version="1.0" id="jdisc">
<search />
<nodes count="1" />
+ {accessControlXml}
</jdisc>
-
<content id="music" version="1.0">
<redundancy>1</redundancy>
<documents>
@@ -40,6 +40,14 @@ class ImplicitIndexingClusterTest {
assertNotNull("Indexing chain not added to jdisc", jdisc.getDocprocChains.allChains().getComponent("indexing"))
}
+ private val accessControlXml =
+ <http>
+ <filtering>
+ <access-control domain="foo" />
+ </filtering>
+ <server id="bar" port="4080" />
+ </http>
+
def buildMultiTenantVespaModel(servicesXml: Elem) = {
val properties = new DeployProperties.Builder().multitenant(true).hostedVespa(true).build()