diff options
author | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-06-07 17:27:22 +0200 |
---|---|---|
committer | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-06-07 17:27:22 +0200 |
commit | ce08379e47b9e02836026d111e1a27681b21c715 (patch) | |
tree | 954830fead4acd24854a231b2d6d202676ac72ce | |
parent | 65937b2191aad18db17403cdbf0b94db7aac7cc7 (diff) |
Move tenant authentication to new module tenant-auth, to override internally
-rw-r--r-- | pom.xml | 1 | ||||
-rw-r--r-- | tenant-auth/OWNERS | 1 | ||||
-rw-r--r-- | tenant-auth/README.md | 1 | ||||
-rw-r--r-- | tenant-auth/pom.xml | 40 | ||||
-rw-r--r-- | tenant-auth/src/main/java/ai/vespa/hosted/auth/Authenticator.java | 73 | ||||
-rw-r--r-- | tenant-auth/src/test/java/ai/vespa/hosted/auth/AuthenticatorTest.java | 5 | ||||
-rw-r--r-- | tenant-cd/pom.xml | 6 | ||||
-rw-r--r-- | tenant-cd/src/main/java/ai/vespa/hosted/cd/EmptyGroup.java | 9 | ||||
-rw-r--r-- | tenant-cd/src/main/java/ai/vespa/hosted/cd/TestConfig.java | 20 | ||||
-rw-r--r-- | tenant-cd/src/main/java/ai/vespa/hosted/cd/http/HttpEndpoint.java | 5 | ||||
-rw-r--r-- | tenant-cd/src/main/java/ai/vespa/hosted/cd/http/Security.java | 43 |
11 files changed, 140 insertions, 64 deletions
@@ -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 <excludedGroups> 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); - } - } - -} |