summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorMorten Tokle <morten.tokle@gmail.com>2018-03-23 10:51:47 +0100
committerGitHub <noreply@github.com>2018-03-23 10:51:47 +0100
commit686694f6a6788c9ebe25f3278c253b0fe015d331 (patch)
tree4278aba80e7160e7d05ec4696fd8b271912886ed /config-model
parent5634f8022c1566522b4aa28fd4d28d5089482234 (diff)
parent0d95d5e3cfce0c4a30f377e100bf22fdecd7733b (diff)
Merge pull request #5351 from vespa-engine/gjoranv/validate-access-control
Require access-control write protection for first deployments.
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java9
-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.java62
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java1
-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.java155
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala10
7 files changed, 247 insertions, 2 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java
index aa8bbda0151..711ea885a36 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.model.admin;
import com.yahoo.cloud.config.SlobroksConfig;
import com.yahoo.cloud.config.ZookeepersConfig;
import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.config.model.ConfigModelContext.ApplicationType;
import com.yahoo.config.model.api.ConfigServerSpec;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.producer.AbstractConfigProducer;
@@ -49,6 +50,8 @@ public class Admin extends AbstractConfigProducer implements Serializable {
private Logserver logserver;
private LogForwarder.Config logForwarderConfig = null;
+ private ApplicationType applicationType = ApplicationType.DEFAULT;
+
public void setLogForwarderConfig(LogForwarder.Config cfg) {
this.logForwarderConfig = cfg;
}
@@ -273,4 +276,10 @@ public class Admin extends AbstractConfigProducer implements Serializable {
return multitenant;
}
+ public void setApplicationType(ApplicationType applicationType) {
+ this.applicationType = applicationType;
+ }
+
+ public ApplicationType getApplicationType() { return applicationType; }
+
}
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 eaad6ad90a0..2a208e45732 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;
@@ -59,6 +60,7 @@ public class Validation {
return validateChanges((VespaModel)currentActiveModel.get(), model,
deployState.validationOverrides(), deployState.getDeployLogger(), deployState.now());
else
+ validateFirstTimeDeployment(model, deployState);
return new ArrayList<>();
}
@@ -79,4 +81,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..d7fb46a8fb2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidator.java
@@ -0,0 +1,62 @@
+// 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.ConfigModelContext.ApplicationType;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.provision.SystemName;
+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) {
+
+ if (! deployState.isHosted()) return;
+ if (! deployState.zone().environment().isProduction()) return;
+ if (model.getAdmin().getApplicationType() != ApplicationType.DEFAULT) return;
+
+ // Temporarily validate apps in CD zones only
+ // TODO: remove, and also remove the zone setting in the unit test
+ if (deployState.zone().system() != SystemName.cd) return;
+
+ 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/builder/xml/dom/DomAdminBuilderBase.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java
index 64cbcaafd9f..a78e9ad30fc 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java
@@ -72,6 +72,7 @@ public abstract class DomAdminBuilderBase extends VespaDomBuilder.DomConfigProdu
FileDistributionConfigProducer fileDistributionConfigProducer = getFileDistributionConfigProducer(parent);
Admin admin = new Admin(parent, monitoring, metrics, legacyMetricsConsumers, multitenant, fileDistributionConfigProducer);
+ admin.setApplicationType(applicationType);
doBuildAdmin(admin, adminElement);
new ModelConfigProvider(admin);
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..82d521516b4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/first/AccessControlValidatorTest.java
@@ -0,0 +1,155 @@
+// 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.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.Zone;
+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 no_http_element_has_same_effect_as_no_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]");
+
+ 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);
+ }
+
+ @Test
+ public void cluster_with_mbus_handler_passes_validation_without_write_protection() throws IOException, SAXException{
+ String servicesXml = joinLines("<services version='1.0'>",
+ " <container id='default' version='1.0'>",
+ " <handler id='foo'>",
+ " <binding>mbus://*/foo</binding>",
+ " </handler>",
+ " </container>",
+ "</services>");
+ DeployState deployState = deployState(servicesXml);
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+
+ new AccessControlValidator().validate(model, deployState);
+ }
+
+ @Test
+ public void write_protection_is_not_required_for_non_default_application_type() throws IOException, SAXException{
+ String servicesXml = joinLines("<services version='1.0' application-type='hosted-infrastructure'>",
+ " <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)
+ .zone(new Zone(SystemName.cd, Environment.prod, RegionName.from("foo")) )// TODO: remove cd setting
+ .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()