From 885cb31bad09bae15067c9c527f051ade6bb2d44 Mon Sep 17 00:00:00 2001 From: Morten Tokle Date: Mon, 23 Nov 2020 14:55:22 +0100 Subject: Create default connector request chain --- .../vespa/model/container/http/AccessControl.java | 31 ++++++++-- .../model/container/xml/ContainerModelBuilder.java | 20 +++++-- .../model/container/xml/AccessControlTest.java | 25 ++++++++ .../http/filter/security/misc/VespaTlsFilter.java | 21 +++++++ .../filter/security/misc/VespaTlsFilterTest.java | 66 ++++++++++++++++++++++ 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilter.java create mode 100644 jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java 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 9bd12350f26..00ff19ae68a 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 @@ -3,6 +3,10 @@ package com.yahoo.vespa.model.container.http; import com.yahoo.component.ComponentId; import com.yahoo.component.ComponentSpecification; +import com.yahoo.component.chain.dependencies.Dependencies; +import com.yahoo.component.chain.model.ChainedComponentModel; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.container.ApplicationContainerCluster; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.component.BindingPattern; @@ -27,11 +31,13 @@ import java.util.Set; */ public class AccessControl { - public enum ClientAuthentication { want, need } + + public enum ClientAuthentication { want, need;} public static final ComponentId ACCESS_CONTROL_CHAIN_ID = ComponentId.fromString("access-control-chain"); - public static final ComponentId ACCESS_CONTROL_EXCLUDED_CHAIN_ID = ComponentId.fromString("access-control-excluded-chain"); + public static final ComponentId ACCESS_CONTROL_EXCLUDED_CHAIN_ID = ComponentId.fromString("access-control-excluded-chain"); + public static final ComponentId DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID = ComponentId.fromString("default-connector-hosted-request-chain"); private static final int HOSTED_CONTAINER_PORT = 4443; // Handlers that are excluded from access control @@ -44,7 +50,6 @@ public class AccessControl { ApplicationContainerCluster.METRICS_V2_HANDLER_CLASS, ApplicationContainerCluster.PROMETHEUS_V1_HANDLER_CLASS ); - public static class Builder { private final String domain; private boolean readEnabled = false; @@ -52,7 +57,6 @@ public class AccessControl { private ClientAuthentication clientAuthentication = ClientAuthentication.need; private final Set excludeBindings = new LinkedHashSet<>(); private Collection> handlers = Collections.emptyList(); - public Builder(String domain) { this.domain = domain; } @@ -112,6 +116,7 @@ public class AccessControl { http.setAccessControl(this); addAccessControlFilterChain(http); addAccessControlExcludedChain(http); + addDefaultHostedRequestChain(http); removeDuplicateBindingsFromAccessControlChain(http); } @@ -119,6 +124,18 @@ public class AccessControl { connectorFactory.setDefaultRequestFilterChain(ACCESS_CONTROL_CHAIN_ID); } + public void configureDefaultHostedConnector(Http http) { + // Set default filter chain on local port + http.getHttpServer() + .get() + .getConnectorFactories() + .stream() + .filter(cf -> cf.getListenPort() == Defaults.getDefaults().vespaWebServicePort()) + .findFirst() + .orElseThrow(() -> new RuntimeException("Could not find default connector")) + .setDefaultRequestFilterChain(DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID); + } + /** returns the excluded bindings as specified in 'access-control' in services.xml **/ public Set excludedBindings() { return excludedBindings; } @@ -148,6 +165,12 @@ public class AccessControl { } } + // Add a filter chain used by default hosted connector + private void addDefaultHostedRequestChain(Http http) { + Chain chain = createChain(DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID); + http.getFilterChains().add(chain); + } + // Remove bindings from access control chain that have binding pattern as a different filter chain private void removeDuplicateBindingsFromAccessControlChain(Http http) { removeDuplicateBindingsFromChain(http, ACCESS_CONTROL_EXCLUDED_CHAIN_ID); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 0e23527c97c..7eea5d8496f 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -85,6 +85,7 @@ import org.w3c.dom.Node; import java.net.URI; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -318,10 +319,16 @@ public class ContainerModelBuilder extends ConfigModelBuilder { if (isHostedTenantApplication(context)) { addHostedImplicitHttpIfNotPresent(cluster); addHostedImplicitAccessControlIfNotPresent(deployState, cluster); + addDefaultConnectorHostedFilterBinding(cluster); addAdditionalHostedConnector(deployState, cluster, context); } } + private void addDefaultConnectorHostedFilterBinding(ApplicationContainerCluster cluster) { + cluster.getHttp().getAccessControl() + .ifPresent(accessControl -> accessControl.configureDefaultHostedConnector(cluster.getHttp())); ; + } + private void addAdditionalHostedConnector(DeployState deployState, ApplicationContainerCluster cluster, ConfigModelContext context) { JettyHttpServer server = cluster.getHttp().getHttpServer().get(); String serverName = server.getComponentId().getName(); @@ -361,10 +368,15 @@ public class ContainerModelBuilder extends ConfigModelBuilder { if(cluster.getHttp() == null) { cluster.setHttp(new Http(new FilterChains(cluster))); } - if(cluster.getHttp().getHttpServer().isEmpty()) { - JettyHttpServer defaultHttpServer = new JettyHttpServer(new ComponentId("DefaultHttpServer"), cluster, cluster.isHostedVespa()); - cluster.getHttp().setHttpServer(defaultHttpServer); - defaultHttpServer.addConnector(new ConnectorFactory.Builder("SearchServer", Defaults.getDefaults().vespaWebServicePort()).build()); + JettyHttpServer httpServer = cluster.getHttp().getHttpServer().orElse(null); + if (httpServer == null) { + httpServer = new JettyHttpServer(new ComponentId("DefaultHttpServer"), cluster, cluster.isHostedVespa()); + cluster.getHttp().setHttpServer(httpServer); + } + int defaultPort = Defaults.getDefaults().vespaWebServicePort(); + boolean defaultConnectorPresent = httpServer.getConnectorFactories().stream().anyMatch(connector -> connector.getListenPort() == defaultPort); + if (!defaultConnectorPresent) { + httpServer.addConnector(new ConnectorFactory.Builder("SearchServer", defaultPort).build()); } } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java index 1ac95ac9a99..4993a51ab74 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessControlTest.java @@ -6,8 +6,10 @@ import com.yahoo.config.model.builder.xml.test.DomBuilderTest; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.container.ApplicationContainer; import com.yahoo.vespa.model.container.http.AccessControl; +import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.http.FilterChains; import com.yahoo.vespa.model.container.http.Http; import com.yahoo.vespa.model.container.http.ssl.HostedSslConnectorFactory; @@ -50,6 +52,7 @@ public class AccessControlTest extends ContainerModelBuilderTestBase { FilterChains filterChains = http.getFilterChains(); assertTrue(filterChains.hasChain(AccessControl.ACCESS_CONTROL_CHAIN_ID)); assertTrue(filterChains.hasChain(AccessControl.ACCESS_CONTROL_EXCLUDED_CHAIN_ID)); + assertTrue(filterChains.hasChain(AccessControl.DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID)); } @Test @@ -297,6 +300,28 @@ public class AccessControlTest extends ContainerModelBuilderTestBase { assertEquals(AccessControl.ClientAuthentication.want, http.getAccessControl().get().clientAuthentication); } + @Test + public void local_connector_has_default_chain() { + Http http = createModelAndGetHttp( + " ", + " ", + " ", + " ", + " "); + + Set actualBindings = getFilterBindings(http, AccessControl.DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID); + assertThat(actualBindings, empty()); + + ConnectorFactory connectorFactory = http.getHttpServer().get().getConnectorFactories().stream() + .filter(cf -> cf.getListenPort() == Defaults.getDefaults().vespaWebServicePort()) + .findAny() + .get(); + + Optional defaultChain = connectorFactory.getDefaultRequestFilterChain(); + assertTrue(defaultChain.isPresent()); + assertEquals(AccessControl.DEFAULT_CONNECTOR_HOSTED_REQUEST_CHAIN_ID, defaultChain.get()); + } + private Http createModelAndGetHttp(String... httpElement) { List servicesXml = new ArrayList<>(); servicesXml.add(""); diff --git a/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilter.java b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilter.java new file mode 100644 index 00000000000..b891212031f --- /dev/null +++ b/jdisc-security-filters/src/main/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilter.java @@ -0,0 +1,21 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.jdisc.http.filter.security.misc; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; + +public class VespaTlsFilter extends JsonSecurityRequestFilterBase { + + @Override + protected Optional filter(DiscFilterRequest request) { + return request.getClientCertificateChain().isEmpty() + ? Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Forbidden to access this path")) + : Optional.empty(); + } +} diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java new file mode 100644 index 00000000000..294126eb349 --- /dev/null +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java @@ -0,0 +1,66 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.jdisc.http.filter.security.misc; + +import com.yahoo.container.jdisc.RequestHandlerTestDriver; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.security.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SignatureAlgorithm; +import com.yahoo.security.X509CertificateBuilder; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +public class VespaTlsFilterTest { + + @Test + public void testFilter() { + assertSuccess(createRequest(List.of(createCertificate()))); + assertForbidden(createRequest(Collections.emptyList())); + } + + private static X509Certificate createCertificate() { + return X509CertificateBuilder + .fromKeypair( + KeyUtils.generateKeypair(KeyAlgorithm.EC), new X500Principal("CN=test"), + Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS), + SignatureAlgorithm.SHA512_WITH_ECDSA, BigInteger.valueOf(1)) + .build(); + } + + private static DiscFilterRequest createRequest(List certChain) { + DiscFilterRequest request = Mockito.mock(DiscFilterRequest.class); + when(request.getClientCertificateChain()).thenReturn(certChain); + when(request.getMethod()).thenReturn("GET"); + when(request.getUri()).thenReturn(URI.create("http://localhost:8080/")); + return request; + } + + private static void assertForbidden(DiscFilterRequest request) { + VespaTlsFilter filter = new VespaTlsFilter(); + RequestHandlerTestDriver.MockResponseHandler handler = new RequestHandlerTestDriver.MockResponseHandler(); + filter.filter(request, handler); + assertEquals(Response.Status.FORBIDDEN, handler.getStatus()); + } + + private static void assertSuccess(DiscFilterRequest request) { + VespaTlsFilter filter = new VespaTlsFilter(); + RequestHandlerTestDriver.MockResponseHandler handler = new RequestHandlerTestDriver.MockResponseHandler(); + filter.filter(request, handler); + assertNull(handler.getResponse()); + } +} -- cgit v1.2.3