diff options
author | Morten Tokle <morten.tokle@gmail.com> | 2018-03-23 10:51:47 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-23 10:51:47 +0100 |
commit | 686694f6a6788c9ebe25f3278c253b0fe015d331 (patch) | |
tree | 4278aba80e7160e7d05ec4696fd8b271912886ed /config-model | |
parent | 5634f8022c1566522b4aa28fd4d28d5089482234 (diff) | |
parent | 0d95d5e3cfce0c4a30f377e100bf22fdecd7733b (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')
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() |