diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-06-26 16:27:31 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2020-06-26 16:27:31 +0200 |
commit | d9da501de962175b6798cef10c1bdbf7d74e827e (patch) | |
tree | d3b847edd091c6adaad5e5f6f56f772325d38f83 /tenant-cd-commons | |
parent | 5060839763d1f7d1210eafc44b33968b99626a42 (diff) |
Move shared tenant-cd-api implementations to new module
Introduce new module tenant-cd-commons. Remove tenant-auth.
Change package name for cloud-tenant-cd to avoid potential package conflict.
Move ApiAuthenticator to hosted-api.
Diffstat (limited to 'tenant-cd-commons')
7 files changed, 280 insertions, 0 deletions
diff --git a/tenant-cd-commons/OWNERS b/tenant-cd-commons/OWNERS new file mode 100644 index 00000000000..0a0d219e4eb --- /dev/null +++ b/tenant-cd-commons/OWNERS @@ -0,0 +1,2 @@ +bjorncs +mortent diff --git a/tenant-cd-commons/README.md b/tenant-cd-commons/README.md new file mode 100644 index 00000000000..b1cd95606c8 --- /dev/null +++ b/tenant-cd-commons/README.md @@ -0,0 +1,3 @@ +# tenant-cd-commons + +Contains shared implementations of APIs/interfaces of `tenant-cd-api`.
\ No newline at end of file diff --git a/tenant-cd-commons/pom.xml b/tenant-cd-commons/pom.xml new file mode 100644 index 00000000000..4d92be95c0b --- /dev/null +++ b/tenant-cd-commons/pom.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.yahoo.vespa</groupId> + <artifactId>tenant-cd-commons</artifactId> + <packaging>jar</packaging> + + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>7-SNAPSHOT</version> + <relativePath>../parent</relativePath> + </parent> + + <dependencies> + <!-- compile --> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>tenant-cd-api</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>security-utils</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + + <!-- compile --> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>hosted-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-provisioning</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project> diff --git a/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/DefaultEndpointAuthenticator.java b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/DefaultEndpointAuthenticator.java new file mode 100644 index 00000000000..89414cc069a --- /dev/null +++ b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/DefaultEndpointAuthenticator.java @@ -0,0 +1,82 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.hosted.cd.commons; + +import ai.vespa.hosted.api.Properties; +import com.yahoo.config.provision.SystemName; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.X509CertificateUtils; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.Optional; +import java.util.logging.Logger; + +import static ai.vespa.hosted.api.Properties.getNonBlankProperty; + +/** + * Authenticates against the hosted Vespa API using private key signatures, and against Vespa applications using mutual TLS. + * + * @author jonmv + */ +public class DefaultEndpointAuthenticator implements EndpointAuthenticator { + + private static final Logger logger = Logger.getLogger(DefaultEndpointAuthenticator.class.getName()); + + /** Don't touch. */ + public DefaultEndpointAuthenticator(@SuppressWarnings("unused") SystemName __) { } + + /** + * If {@code System.getProperty("vespa.test.credentials.root")} is set, key and certificate files + * "key" and "cert" in that directory are used; otherwise, the system default SSLContext is returned. + */ + @Override + public SSLContext sslContext() { + try { + Path certificateFile = null; + Path privateKeyFile = null; + Optional<String> credentialsRootProperty = getNonBlankProperty("vespa.test.credentials.root"); + if (credentialsRootProperty.isPresent()) { + Path credentialsRoot = Path.of(credentialsRootProperty.get()); + certificateFile = credentialsRoot.resolve("cert"); + privateKeyFile = credentialsRoot.resolve("key"); + } + else { + if (Properties.dataPlaneCertificateFile().isPresent()) + certificateFile = Properties.dataPlaneCertificateFile().get(); + if (Properties.dataPlaneKeyFile().isPresent()) + privateKeyFile = Properties.dataPlaneKeyFile().get(); + } + if (certificateFile != null && privateKeyFile != null) { + X509Certificate certificate = X509CertificateUtils.fromPem(new String(Files.readAllBytes(certificateFile))); + if ( Instant.now().isBefore(certificate.getNotBefore().toInstant()) + || Instant.now().isAfter(certificate.getNotAfter().toInstant())) + throw new IllegalStateException("Certificate at '" + certificateFile + "' is valid between " + + certificate.getNotBefore() + " and " + certificate.getNotAfter() + " — not now."); + + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(new String(Files.readAllBytes(privateKeyFile))); + return new SslContextBuilder().withKeyStore(privateKey, certificate).build(); + } + logger.warning( "##################################################################################\n" + + "# Data plane key and/or certificate missing; please specify #\n" + + "# '-DdataPlaneCertificateFile=/path/to/certificate' and #\n" + + "# '-DdataPlaneKeyFile=/path/to/private_key'. #\n" + + "# Trying the default SSLContext, but this will most likely cause HTTP error 401. #\n" + + "##################################################################################"); + return SSLContext.getDefault(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/EndpointAuthenticator.java b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/EndpointAuthenticator.java new file mode 100644 index 00000000000..e7936ddea7a --- /dev/null +++ b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/EndpointAuthenticator.java @@ -0,0 +1,34 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.hosted.cd.commons; + +import javax.net.ssl.SSLContext; +import java.net.http.HttpRequest; +import java.security.NoSuchAlgorithmException; + +/** + * Adds environment dependent authentication to HTTP request against Vespa deployments. + * + * An implementation typically needs to override either of the methods in this interface, + * and needs to run in different environments, e.g., local user testing and automatic testing + * in a deployment pipeline. + * + * @author jonmv + */ +public interface EndpointAuthenticator { + + /** Returns an SSLContext which provides authentication against a Vespa endpoint. */ + default SSLContext sslContext() { + try { + return SSLContext.getDefault(); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** Adds necessary authentication data to the given HTTP request builder, to pass the data plane of a Vespa endpoint. */ + default HttpRequest.Builder authenticated(HttpRequest.Builder request) { + return request; + } + +} diff --git a/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpDeployment.java b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpDeployment.java new file mode 100644 index 00000000000..90a33bcb513 --- /dev/null +++ b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpDeployment.java @@ -0,0 +1,36 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.hosted.cd.commons; + +import ai.vespa.hosted.cd.Deployment; +import ai.vespa.hosted.cd.Endpoint; + +import java.net.URI; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +/** + * A remote deployment of a Vespa application, reachable over HTTP. Contains {@link HttpEndpoint}s. + * + * @author jonmv + */ +public class HttpDeployment implements Deployment { + + private final Map<String, Endpoint> endpoints; + + /** Creates a representation of the given deployment endpoints, using the authenticator for data plane access. */ + public HttpDeployment(Map<String, URI> endpoints, EndpointAuthenticator authenticator) { + this.endpoints = endpoints.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(), + entry -> new HttpEndpoint(entry.getValue(), authenticator))); + } + + @Override + public Endpoint endpoint(String id) { + if ( ! endpoints.containsKey(id)) + throw new NoSuchElementException("No cluster with id '" + id + "'"); + + return endpoints.get(id); + } + +} diff --git a/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpEndpoint.java b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpEndpoint.java new file mode 100644 index 00000000000..cf8865df878 --- /dev/null +++ b/tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpEndpoint.java @@ -0,0 +1,67 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.hosted.cd.commons; + +import ai.vespa.hosted.cd.Endpoint; + +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.net.URLEncoder.encode; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +/** + * A remote endpoint in a {@link HttpDeployment} of a Vespa application, reachable over HTTP. + * + * @author jonmv + */ +public class HttpEndpoint implements Endpoint { + + private final URI endpoint; + private final HttpClient client; + private final EndpointAuthenticator authenticator; + + public HttpEndpoint(URI endpoint, EndpointAuthenticator authenticator) { + this.endpoint = requireNonNull(endpoint); + this.authenticator = requireNonNull(authenticator); + SSLParameters sslParameters = new SSLParameters(); + sslParameters.setProtocols(new String[] {"TLSv1.2" }); + this.client = HttpClient.newBuilder() + .sslContext(authenticator.sslContext()) + .connectTimeout(Duration.ofSeconds(5)) + .version(HttpClient.Version.HTTP_1_1) + .sslParameters(sslParameters) + .build(); + } + + @Override + public URI uri() { + return endpoint; + } + + @Override + public <T> HttpResponse<T> send(HttpRequest.Builder request, HttpResponse.BodyHandler<T> handler) { + try { + return client.send(authenticator.authenticated(request).build(), handler); + } + catch (IOException | InterruptedException e) { + throw new RuntimeException(request.build() + " failed: " + e.getMessage(), e); + } + } + + @Override + public HttpRequest.Builder request(String path, Map<String, String> properties) { + return HttpRequest.newBuilder(endpoint.resolve(path + + properties.entrySet().stream() + .map(entry -> encode(entry.getKey(), UTF_8) + "=" + encode(entry.getValue(), UTF_8)) + .collect(Collectors.joining("&", path.contains("?") ? "&" : "?", "")))); + } + +} |