diff options
author | gjoranv <gv@oath.com> | 2018-03-14 15:48:01 +0100 |
---|---|---|
committer | gjoranv <gv@oath.com> | 2018-03-19 12:13:33 +0100 |
commit | 6ae27a8d968d0399796c53f46ccfba607f70d977 (patch) | |
tree | 626a48a6cfc728f0d64d429f6ec1d938a18fcda7 /config-model | |
parent | f11f286bf449a56f3618e8d8ad6d4a6be817b3bd (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')
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() |