summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pom.xml1
-rw-r--r--tenant-auth/OWNERS1
-rw-r--r--tenant-auth/README.md1
-rw-r--r--tenant-auth/pom.xml40
-rw-r--r--tenant-auth/src/main/java/ai/vespa/hosted/auth/Authenticator.java73
-rw-r--r--tenant-auth/src/test/java/ai/vespa/hosted/auth/AuthenticatorTest.java5
-rw-r--r--tenant-cd/pom.xml6
-rw-r--r--tenant-cd/src/main/java/ai/vespa/hosted/cd/EmptyGroup.java9
-rw-r--r--tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java20
-rw-r--r--tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java5
-rw-r--r--tenant-cd/src/main/java/ai/vespa/hosted/cd/http/Security.java43
11 files changed, 140 insertions, 64 deletions
diff --git a/pom.xml b/pom.xml
index 120547fbfed..984c0cdf7a2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -120,6 +120,7 @@
<module>standalone-container</module>
<module>statistics</module>
<module>storage</module>
+ <module>tenant-auth</module>
<module>tenant-base</module>
<module>tenant-cd</module>
<module>testutil</module>
diff --git a/tenant-auth/OWNERS b/tenant-auth/OWNERS
new file mode 100644
index 00000000000..d0a102ecbf4
--- /dev/null
+++ b/tenant-auth/OWNERS
@@ -0,0 +1 @@
+jonmv
diff --git a/tenant-auth/README.md b/tenant-auth/README.md
new file mode 100644
index 00000000000..0514b68400e
--- /dev/null
+++ b/tenant-auth/README.md
@@ -0,0 +1 @@
+# Utilities that authenticate users to the hosted Vespa API, or to hosted Vespa applications.
diff --git a/tenant-auth/pom.xml b/tenant-auth/pom.xml
new file mode 100644
index 00000000000..be8b42dd6c2
--- /dev/null
+++ b/tenant-auth/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2019 Oath Inc. 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/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>7-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>tenant-auth</artifactId>
+ <description>Provides resources for authenticating with the hosted Vespa API or application containers</description>
+
+ <dependencies>
+ <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>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>security-utils</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/tenant-auth/src/main/java/ai/vespa/hosted/auth/Authenticator.java b/tenant-auth/src/main/java/ai/vespa/hosted/auth/Authenticator.java
new file mode 100644
index 00000000000..6ecf1100630
--- /dev/null
+++ b/tenant-auth/src/main/java/ai/vespa/hosted/auth/Authenticator.java
@@ -0,0 +1,73 @@
+package ai.vespa.hosted.auth;
+
+import ai.vespa.hosted.api.ControllerHttpClient;
+import com.yahoo.config.provision.ApplicationId;
+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.net.URI;
+import java.net.http.HttpRequest;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Authenticates {@link HttpRequest}s against a hosted Vespa application based on mutual TLS.
+ *
+ * @author jonmv
+ */
+public class Authenticator {
+
+ /** Returns an SSLContext from "key" and "cert" files found under {@code System.getProperty("vespa.test.credentials.root")}. */
+ public SSLContext sslContext() {
+ try {
+ Path credentialsRoot = Path.of(System.getProperty("vespa.test.credentials.root"));
+ Path certificateFile = credentialsRoot.resolve("cert");
+ Path privateKeyFile = credentialsRoot.resolve("key");
+
+ 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();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public HttpRequest.Builder authenticated(HttpRequest.Builder request) {
+ return request;
+ }
+
+ ApplicationId id = ApplicationId.from(requireNonBlankProperty("tenant"),
+ requireNonBlankProperty("application"),
+ getNonBlankProperty("instance").orElse("default"));
+
+ URI endpoint = URI.create(requireNonBlankProperty("endpoint"));
+ Path privateKeyFile = Paths.get(requireNonBlankProperty("privateKeyFile"));
+ Optional<Path> certificateFile = getNonBlankProperty("certificateFile").map(Paths::get);
+
+ ControllerHttpClient controller = certificateFile.isPresent()
+ ? ControllerHttpClient.withKeyAndCertificate(endpoint, privateKeyFile, certificateFile.get())
+ : ControllerHttpClient.withSignatureKey(endpoint, privateKeyFile, id);
+
+ static Optional<String> getNonBlankProperty(String name) {
+ return Optional.ofNullable(System.getProperty(name)).filter(value -> ! value.isBlank());
+ }
+
+ static String requireNonBlankProperty(String name) {
+ return getNonBlankProperty(name).orElseThrow(() -> new IllegalStateException("Missing required property '" + name + "'"));
+ }
+
+}
diff --git a/tenant-auth/src/test/java/ai/vespa/hosted/auth/AuthenticatorTest.java b/tenant-auth/src/test/java/ai/vespa/hosted/auth/AuthenticatorTest.java
new file mode 100644
index 00000000000..ff4bebce3ff
--- /dev/null
+++ b/tenant-auth/src/test/java/ai/vespa/hosted/auth/AuthenticatorTest.java
@@ -0,0 +1,5 @@
+package ai.vespa.hosted.auth;
+
+public class AuthenticatorTest {
+
+}
diff --git a/tenant-cd/pom.xml b/tenant-cd/pom.xml
index ba93bbe407d..7cc2c9a2d5b 100644
--- a/tenant-cd/pom.xml
+++ b/tenant-cd/pom.xml
@@ -40,6 +40,12 @@
<dependency>
<groupId>com.yahoo.vespa</groupId>
+ <artifactId>tenant-auth</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
<artifactId>hosted-api</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/EmptyGroup.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/EmptyGroup.java
new file mode 100644
index 00000000000..8deca3cfb11
--- /dev/null
+++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/EmptyGroup.java
@@ -0,0 +1,9 @@
+package ai.vespa.hosted.cd;
+
+/**
+ * The Surefire configuration element &lt;excludedGroups&gt; requires a non-empty argument to reset another.
+ * This class serves that purpose. Without it, no tests run in the various integration test profiles.
+ *
+ * @author jonmv
+ */
+public interface EmptyGroup { }
diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java
index ed9aea0e9b0..36c14a38b37 100644
--- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java
+++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java
@@ -81,18 +81,6 @@ public class TestConfig {
}
static TestConfig fromController() {
- ApplicationId id = ApplicationId.from(requireNonBlankProperty("tenant"),
- requireNonBlankProperty("application"),
- getNonBlankProperty("instance").orElse("default"));
-
- URI endpoint = URI.create(requireNonBlankProperty("endpoint"));
- Path privateKeyFile = Paths.get(requireNonBlankProperty("privateKeyFile"));
- Optional<Path> certificateFile = getNonBlankProperty("certificateFile").map(Paths::get);
-
- ControllerHttpClient controller = certificateFile.isPresent()
- ? ControllerHttpClient.withKeyAndCertificate(endpoint, privateKeyFile, certificateFile.get())
- : ControllerHttpClient.withSignatureKey(endpoint, privateKeyFile, id);
-
return null;
}
@@ -110,12 +98,4 @@ public class TestConfig {
return new TestConfig(application, zone, system, endpoints);
}
- static Optional<String> getNonBlankProperty(String name) {
- return Optional.ofNullable(System.getProperty(name)).filter(value -> ! value.isBlank());
- }
-
- static String requireNonBlankProperty(String name) {
- return getNonBlankProperty(name).orElseThrow(() -> new IllegalStateException("Missing required property '" + name + "'"));
- }
-
}
diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java
index 7b4f09650ce..e0d3787a21c 100644
--- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java
+++ b/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java
@@ -1,5 +1,6 @@
package ai.vespa.hosted.cd.http;
+import ai.vespa.hosted.auth.Authenticator;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.JsonDecoder;
import com.yahoo.slime.Slime;
@@ -28,11 +29,13 @@ public class HttpEndpoint implements TestEndpoint {
private final URI endpoint;
private final HttpClient client;
+ private final Authenticator authenticator;
public HttpEndpoint(URI endpoint) {
this.endpoint = requireNonNull(endpoint);
+ this.authenticator = new Authenticator();
this.client = HttpClient.newBuilder()
- .sslContext(Security.sslContext())
+ .sslContext(authenticator.sslContext())
.connectTimeout(Duration.ofSeconds(5))
.version(HttpClient.Version.HTTP_1_1)
.build();
diff --git a/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/Security.java b/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/Security.java
deleted file mode 100644
index b4524e3922a..00000000000
--- a/tenant-cd/src/main/java/ai/vespa/hosted/cd/http/Security.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package ai.vespa.hosted.cd.http;
-
-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.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-
-/**
- * Miscellaneous related to HTTP security and authentication.
- */
-public class Security {
-
- private Security() { }
-
- /** Returns an SSLContext from "key" and "cert" files found under {@code System.getProperty("vespa.test.credentials.root")}. */
- public static SSLContext sslContext() {
- try {
- Path credentialsRoot = Path.of(System.getProperty("vespa.test.credentials.root"));
- Path certificateFile = credentialsRoot.resolve("cert");
- Path privateKeyFile = credentialsRoot.resolve("key");
-
- 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();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
-}