summaryrefslogtreecommitdiffstats
path: root/tenant-cd-commons
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2020-06-26 16:27:31 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2020-06-26 16:27:31 +0200
commitd9da501de962175b6798cef10c1bdbf7d74e827e (patch)
treed3b847edd091c6adaad5e5f6f56f772325d38f83 /tenant-cd-commons
parent5060839763d1f7d1210eafc44b33968b99626a42 (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')
-rw-r--r--tenant-cd-commons/OWNERS2
-rw-r--r--tenant-cd-commons/README.md3
-rw-r--r--tenant-cd-commons/pom.xml56
-rw-r--r--tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/DefaultEndpointAuthenticator.java82
-rw-r--r--tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/EndpointAuthenticator.java34
-rw-r--r--tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpDeployment.java36
-rw-r--r--tenant-cd-commons/src/main/java/ai/vespa/hosted/cd/commons/HttpEndpoint.java67
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("?") ? "&" : "?", ""))));
+ }
+
+}