diff options
Diffstat (limited to 'athenz-identity-provider-service')
17 files changed, 1133 insertions, 0 deletions
diff --git a/athenz-identity-provider-service/OWNERS b/athenz-identity-provider-service/OWNERS new file mode 100644 index 00000000000..569bf1cc3a1 --- /dev/null +++ b/athenz-identity-provider-service/OWNERS @@ -0,0 +1 @@ +bjorncs diff --git a/athenz-identity-provider-service/pom.xml b/athenz-identity-provider-service/pom.xml new file mode 100644 index 00000000000..e3998b02ad0 --- /dev/null +++ b/athenz-identity-provider-service/pom.xml @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<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> + <artifactId>athenz-identity-provider-service</artifactId> + <packaging>container-plugin</packaging> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + </parent> + <dependencies> + <!-- COMPILE --> + <dependency> + <groupId>com.yahoo.athenz</groupId> + <artifactId>athenz-zms-java-client</artifactId> + <scope>compile</scope> + <exclusions> + <!--Exclude all Jersey bundles provided by JDisc--> + <exclusion> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + </exclusion> + <exclusion> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + </exclusion> + <!-- BouncyCastle is not bundled due to class loading issues + when security provider is registered from inside a OSGi bundle --> + <exclusion> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + </exclusion> + <exclusion> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.yahoo.athenz</groupId> + <artifactId>athenz-zts-java-client</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-jsr310</artifactId> + <scope>compile</scope> + </dependency> + + <!-- PROVIDED --> + <!-- BouncyCastle should be available through jdisc_http_service at runtime --> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-dev</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <scope>provided</scope> + </dependency> + + <!-- TEST --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.4.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + <version>4.4.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <extensions>true</extensions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + </plugins> + </build> + +</project> diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java new file mode 100644 index 00000000000..e3b31263421 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderService.java @@ -0,0 +1,174 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice; + +import com.google.inject.Inject; +import com.yahoo.athenz.auth.impl.PrincipalAuthority; +import com.yahoo.athenz.auth.impl.SimpleServiceIdentityProvider; +import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.athenz.zts.InstanceRefreshRequest; +import com.yahoo.athenz.zts.ZTSClient; +import com.yahoo.component.AbstractComponent; +import com.yahoo.jdisc.http.ssl.ReaderForPath; +import com.yahoo.jdisc.http.ssl.pem.PemKeyStore; +import com.yahoo.jdisc.http.ssl.pem.PemSslKeyStore; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.FileBackedKeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.InstanceValidator; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.ProviderServiceServlet; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.StatusServlet; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import java.io.StringReader; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * A component acting as both SIA for configserver and provides a lightweight Jetty instance hosting the InstanceConfirmation API + * + * @author bjorncs + */ +public class AthenzInstanceProviderService extends AbstractComponent { + + private static final Logger log = Logger.getLogger(AthenzInstanceProviderService.class.getName()); + + private final ScheduledExecutorService scheduler; + private final Server jetty; + + @Inject + public AthenzInstanceProviderService(AthenzProviderServiceConfig config) { + this(config, new FileBackedKeyProvider(config.keyPathPrefix()), Executors.newSingleThreadScheduledExecutor()); + } + + AthenzInstanceProviderService(AthenzProviderServiceConfig config, + KeyProvider keyProvider, + ScheduledExecutorService scheduler) { + this.scheduler = scheduler; + SslContextFactory sslContextFactory = createSslContextFactory(); + this.jetty = createJettyServer(config.port(), config.apiPath(), keyProvider, sslContextFactory); + AthenzCertificateUpdater reloader = new AthenzCertificateUpdater( + sslContextFactory, keyProvider, config); + scheduler.scheduleAtFixedRate(reloader, 0, 1, TimeUnit.DAYS); + try { + jetty.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Server createJettyServer(int port, String apiPath, + KeyProvider keyProvider, SslContextFactory sslContextFactory) { + Server server = new Server(); + ServerConnector connector = new ServerConnector(server, sslContextFactory); + connector.setPort(port); + server.addConnector(connector); + + ServletHandler handler = new ServletHandler(); + ProviderServiceServlet providerServiceServlet = + new ProviderServiceServlet(new InstanceValidator(keyProvider)); + handler.addServletWithMapping(new ServletHolder(providerServiceServlet), apiPath); + handler.addServletWithMapping(StatusServlet.class, "/status.html"); + server.setHandler(handler); + return server; + + } + + private static SslContextFactory createSslContextFactory() { + try { + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setWantClientAuth(true); + sslContextFactory.setProtocol("TLS"); + sslContextFactory.setKeyManagerFactoryAlgorithm("SunX509"); + return sslContextFactory; + } catch (Exception e) { + throw new IllegalArgumentException("Failed to create SSL context factory: " + e.getMessage(), e); + } + } + + private static class AthenzCertificateUpdater implements Runnable { + + private static final Logger log = Logger.getLogger(AthenzCertificateUpdater.class.getName()); + + private final SslContextFactory sslContextFactory; + private final KeyProvider keyProvider; + private final AthenzProviderServiceConfig config; + + private AthenzCertificateUpdater(SslContextFactory sslContextFactory, + KeyProvider keyProvider, + AthenzProviderServiceConfig config) { + this.sslContextFactory = sslContextFactory; + this.keyProvider = keyProvider; + this.config = config; + } + + @Override + public void run() { + try { + log.log(LogLevel.INFO, "Updating Athenz certificate through ZTS"); + String privateKey = keyProvider.getPrivateKey(config.keyVersion()); + String certificate = getCertificateFromZTS(Crypto.loadPrivateKey(privateKey)); + final KeyStore keyStore = + new PemSslKeyStore( + new PemKeyStore.KeyStoreLoadParameter( + new ReaderForPath(new StringReader(certificate), null), + new ReaderForPath(new StringReader(privateKey), null))) + .loadJavaKeyStore(); + sslContextFactory.reload(sslContextFactory -> sslContextFactory.setKeyStore(keyStore)); + log.log(LogLevel.INFO, "Athenz certificate reload successfully completed"); + } catch (Exception e) { + log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e); + } + } + + private String getCertificateFromZTS(PrivateKey privateKey) { + SimpleServiceIdentityProvider identityProvider = new SimpleServiceIdentityProvider( + new AthenzPrincipalAuthority(config.athenzPrincipalHeaderName()), config.domain(), config.serviceName(), + privateKey, Integer.toString(config.keyVersion()), TimeUnit.MINUTES.toSeconds(10)); + ZTSClient ztsClient = new ZTSClient( + config.ztsUrl(), config.domain(), config.serviceName(), identityProvider); + InstanceRefreshRequest req = ZTSClient.generateInstanceRefreshRequest( + config.domain(), config.serviceName(), privateKey, config.certDnsSuffix(), (int)TimeUnit.DAYS.toSeconds(30)); + return ztsClient.postInstanceRefreshRequest(config.domain(), config.serviceName(), req).getCertificate(); + } + + private static class AthenzPrincipalAuthority extends PrincipalAuthority { + private final String headerName; + + public AthenzPrincipalAuthority(String headerName) { + this.headerName = headerName; + } + + @Override + public String getHeader() { + return headerName; + } + } + } + + @Override + public void deconstruct() { + try { + log.log(LogLevel.INFO, "Deconstructing Athenz provider service"); + scheduler.shutdown(); + jetty.stop(); + if (!scheduler.awaitTermination(1, TimeUnit.MINUTES)) { + log.log(LogLevel.ERROR, "Failed to stop certificate updater"); + } + } catch (InterruptedException e) { + log.log(LogLevel.ERROR, "Failed to stop certificate updater: " + e.getMessage(), e); + } catch (Exception e) { + log.log(LogLevel.ERROR, "Failed to stop Jetty: " + e.getMessage(), e); + } finally { + super.deconstruct(); + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java new file mode 100644 index 00000000000..f03f8415586 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/FileBackedKeyProvider.java @@ -0,0 +1,40 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; + +/** + * @author bjorncs + */ +public class FileBackedKeyProvider implements KeyProvider { + + private final String keyPathPrefix; + + public FileBackedKeyProvider(String keyPathPrefix) { + this.keyPathPrefix = keyPathPrefix; + } + + @Override + public String getPrivateKey(int version) { + return loadKey(new File(keyPathPrefix + ".priv." + version)); + } + + @Override + public String getPublicKey(int version) { + return loadKey(new File(keyPathPrefix + ".pub." + version)); + } + + private static String loadKey(File file) { + try { + if (!file.exists() || !file.isFile()) { + throw new IllegalArgumentException("Key missing: " + file.getAbsolutePath()); + } + return new String(Files.readAllBytes(file.toPath())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java new file mode 100644 index 00000000000..da8a4afebd8 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/InstanceValidator.java @@ -0,0 +1,56 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Base64; +import java.util.logging.Logger; + +/** + * Verifies that the instance's identity document is valid + * + * @author bjorncs + */ +public class InstanceValidator { + + private static final Logger log = Logger.getLogger(InstanceValidator.class.getName()); + + private final KeyProvider keyProvider; + + public InstanceValidator(KeyProvider keyProvider) { + this.keyProvider = keyProvider; + } + + public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { + SignedIdentityDocument signedIdentityDocument = instanceConfirmation.signedIdentityDocument; + ProviderUniqueId providerUniqueId = signedIdentityDocument.identityDocument.providerUniqueId; + log.log(LogLevel.INFO, () -> String.format("Validating instance %s.", providerUniqueId)); + PublicKey publicKey = Crypto.loadPublicKey(keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion)); + if (isSignatureValid(publicKey, signedIdentityDocument.rawIdentityDocument, signedIdentityDocument.signature)) { + log.log(LogLevel.INFO, () -> String.format("Instance %s is valid.", providerUniqueId)); + return true; + } + log.log(LogLevel.ERROR, () -> String.format("Instance %s has invalid signature.", providerUniqueId)); + return false; + } + + private static boolean isSignatureValid(PublicKey publicKey, String rawIdentityDocument, String signature) { + try { + Signature signatureVerifier = Signature.getInstance("SHA512withRSA"); + signatureVerifier.initVerify(publicKey); + signatureVerifier.update(rawIdentityDocument.getBytes()); + return signatureVerifier.verify(Base64.getDecoder().decode(signature)); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new RuntimeException(e); + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java new file mode 100644 index 00000000000..8c807405693 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/KeyProvider.java @@ -0,0 +1,11 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +/** + * @author bjorncs + */ +public interface KeyProvider { + String getPrivateKey(int version); + + String getPublicKey(int version); +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java new file mode 100644 index 00000000000..a3a4d97706d --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/ProviderServiceServlet.java @@ -0,0 +1,61 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * A Servlet implementing the Athenz Service Provider InstanceConfirmation API + * + * @author bjorncs + */ +public class ProviderServiceServlet extends HttpServlet { + + private static final Logger log = Logger.getLogger(ProviderServiceServlet.class.getName()); + + private final InstanceValidator instanceValidator; + + public ProviderServiceServlet(InstanceValidator instanceValidator) { + this.instanceValidator = instanceValidator; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // TODO Validate that request originates from ZTS + try { + String confirmationContent = toString(req.getReader()); + log.log(LogLevel.DEBUG, () -> "Confirmation content: " + confirmationContent); + InstanceConfirmation instanceConfirmation = + Utils.getMapper().readValue(confirmationContent, InstanceConfirmation.class); + log.log(LogLevel.DEBUG, () -> "Parsed confirmation content: " + instanceConfirmation.toString()); + if (!instanceValidator.isValidInstance(instanceConfirmation)) { + log.log(LogLevel.ERROR, "Invalid instance: " + instanceConfirmation); + resp.setStatus(HttpServletResponse.SC_FORBIDDEN); + } else { + resp.setStatus(HttpServletResponse.SC_OK); + } + } catch (JsonParseException | JsonMappingException e) { + log.log(LogLevel.ERROR, "InstanceConfirmation is not valid JSON", e); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + } + + private static String toString(Reader reader) throws IOException { + try (BufferedReader bufferedReader = new BufferedReader(reader)) { + return bufferedReader.lines().collect(Collectors.joining("\n")); + } + } + +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java new file mode 100644 index 00000000000..fd5ba5843aa --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/StatusServlet.java @@ -0,0 +1,21 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A simple status servlet that should return status code 200 as long as the provider service servlet is up. + * + * @author bjorncs + */ +public class StatusServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java new file mode 100644 index 00000000000..d81ec183fd4 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/Utils.java @@ -0,0 +1,23 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + * @author bjorncs + */ +public class Utils { + + private static final ObjectMapper mapper = createObjectMapper(); + + public static ObjectMapper getMapper() { + return mapper; + } + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java new file mode 100644 index 00000000000..0b4fc38b00d --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/IdentityDocument.java @@ -0,0 +1,70 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; +import java.util.Objects; + +/** + * @author bjorncs + */ +public class IdentityDocument { + + @JsonProperty("athens-domain") + public final String athensDomain; + @JsonProperty("athens-service") + public final String athensService; + @JsonProperty("provider-unique-id") + public final ProviderUniqueId providerUniqueId; + @JsonProperty("configserver-hostname") + public final String configServerHostname; + @JsonProperty("instance-hostname") + public final String instanceHostname; + @JsonProperty("created-at") + public final Instant createdAt; + + public IdentityDocument(@JsonProperty("athens-domain") String athensDomain, + @JsonProperty("athens-service") String athensService, + @JsonProperty("provider-unique-id") ProviderUniqueId providerUniqueId, + @JsonProperty("configserver-hostname") String configServerHostname, + @JsonProperty("instance-hostname") String instanceHostname, + @JsonProperty("created-at") Instant createdAt) { + this.athensDomain = athensDomain; + this.athensService = athensService; + this.providerUniqueId = providerUniqueId; + this.configServerHostname = configServerHostname; + this.instanceHostname = instanceHostname; + this.createdAt = createdAt; + } + + @Override + public String toString() { + return "IdentityDocument{" + + "athensDomain='" + athensDomain + '\'' + + ", athensService='" + athensService + '\'' + + ", providerUniqueId=" + providerUniqueId + + ", configServerHostname='" + configServerHostname + '\'' + + ", instanceHostname='" + instanceHostname + '\'' + + ", createdAt=" + createdAt + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IdentityDocument that = (IdentityDocument) o; + return Objects.equals(athensDomain, that.athensDomain) && + Objects.equals(athensService, that.athensService) && + Objects.equals(providerUniqueId, that.providerUniqueId) && + Objects.equals(configServerHostname, that.configServerHostname) && + Objects.equals(instanceHostname, that.instanceHostname) && + Objects.equals(createdAt, that.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(athensDomain, athensService, providerUniqueId, configServerHostname, instanceHostname, createdAt); + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java new file mode 100644 index 00000000000..ade42968e58 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/InstanceConfirmation.java @@ -0,0 +1,99 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * InstanceConfirmation object as per Athenz InstanceConfirmation API. + * + * @author bjorncs + */ +public class InstanceConfirmation { + + @JsonProperty("provider") public final String provider; + @JsonProperty("domain") public final String domain; + @JsonProperty("service") public final String service; + + @JsonProperty("attestationData") @JsonSerialize(using = SignedIdentitySerializer.class) + public final SignedIdentityDocument signedIdentityDocument; + @JsonUnwrapped public final Map<String, Object> attributes = new HashMap<>(); // optional attributes that Athenz may provide + + @JsonCreator + public InstanceConfirmation(@JsonProperty("provider") String provider, + @JsonProperty("domain") String domain, + @JsonProperty("service") String service, + @JsonProperty("attestationData") @JsonDeserialize(using = SignedIdentityDeserializer.class) + SignedIdentityDocument signedIdentityDocument) { + this.provider = provider; + this.domain = domain; + this.service = service; + this.signedIdentityDocument = signedIdentityDocument; + } + + @JsonAnySetter + public void set(String name, Object value) { + attributes.put(name, value); + } + + @Override + public String toString() { + return "InstanceConfirmation{" + + "provider='" + provider + '\'' + + ", domain='" + domain + '\'' + + ", service='" + service + '\'' + + ", signedIdentityDocument='" + signedIdentityDocument + '\'' + + ", attributes=" + attributes + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InstanceConfirmation that = (InstanceConfirmation) o; + return Objects.equals(provider, that.provider) && + Objects.equals(domain, that.domain) && + Objects.equals(service, that.service) && + Objects.equals(signedIdentityDocument, that.signedIdentityDocument) && + Objects.equals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(provider, domain, service, signedIdentityDocument, attributes); + } + + public static class SignedIdentityDeserializer extends JsonDeserializer<SignedIdentityDocument> { + @Override + public SignedIdentityDocument deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + String value = jsonParser.getValueAsString(); + return Utils.getMapper().readValue(value, SignedIdentityDocument.class); + } + } + + public static class SignedIdentitySerializer extends JsonSerializer<SignedIdentityDocument> { + @Override + public void serialize( + SignedIdentityDocument document, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Utils.getMapper().writeValueAsString(document)); + } + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java new file mode 100644 index 00000000000..4c09dd917a4 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/ProviderUniqueId.java @@ -0,0 +1,68 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * @author bjorncs + */ +public class ProviderUniqueId { + + @JsonProperty("tenant") public final String tenant; + @JsonProperty("application") public final String application; + @JsonProperty("environment") public final String environment; + @JsonProperty("region") public final String region; + @JsonProperty("instance") public final String instance; + @JsonProperty("cluster-id") public final String clusterId; + @JsonProperty("cluster-index") public final int clusterIndex; + + public ProviderUniqueId(@JsonProperty("tenant") String tenant, + @JsonProperty("application") String application, + @JsonProperty("environment") String environment, + @JsonProperty("region") String region, + @JsonProperty("instance") String instance, + @JsonProperty("cluster-id") String clusterId, + @JsonProperty("cluster-index") int clusterIndex) { + this.tenant = tenant; + this.application = application; + this.environment = environment; + this.region = region; + this.instance = instance; + this.clusterId = clusterId; + this.clusterIndex = clusterIndex; + } + + @Override + public String toString() { + return "ProviderUniqueId{" + + "tenant='" + tenant + '\'' + + ", application='" + application + '\'' + + ", environment='" + environment + '\'' + + ", region='" + region + '\'' + + ", instance='" + instance + '\'' + + ", clusterId='" + clusterId + '\'' + + ", clusterIndex=" + clusterIndex + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProviderUniqueId that = (ProviderUniqueId) o; + return clusterIndex == that.clusterIndex && + Objects.equals(tenant, that.tenant) && + Objects.equals(application, that.application) && + Objects.equals(environment, that.environment) && + Objects.equals(region, that.region) && + Objects.equals(instance, that.instance) && + Objects.equals(clusterId, that.clusterId); + } + + @Override + public int hashCode() { + return Objects.hash(tenant, application, environment, region, instance, clusterId, clusterIndex); + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java new file mode 100644 index 00000000000..df1bfe772e8 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/impl/model/SignedIdentityDocument.java @@ -0,0 +1,72 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Base64; +import java.util.Objects; + +/** + * @author bjorncs + */ +public class SignedIdentityDocument { + + @JsonProperty("identity-document")public final String rawIdentityDocument; + @JsonIgnore public final IdentityDocument identityDocument; + @JsonProperty("signature") public final String signature; + @JsonProperty("signing-key-version") public final int signingKeyVersion; + @JsonProperty("document-version") public final int documentVersion; + + @JsonCreator + public SignedIdentityDocument(@JsonProperty("identity-document") String rawIdentityDocument, + @JsonProperty("signature") String signature, + @JsonProperty("signing-key-version") int signingKeyVersion, + @JsonProperty("document-version") int documentVersion) { + this.rawIdentityDocument = rawIdentityDocument; + this.identityDocument = parseIdentityDocument(rawIdentityDocument); + this.signature = signature; + this.signingKeyVersion = signingKeyVersion; + this.documentVersion = documentVersion; + } + + private static IdentityDocument parseIdentityDocument(String rawIdentityDocument) { + try { + return Utils.getMapper().readValue(Base64.getDecoder().decode(rawIdentityDocument), IdentityDocument.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String toString() { + return "SignedIdentityDocument{" + + "rawIdentityDocument='" + rawIdentityDocument + '\'' + + ", identityDocument=" + identityDocument + + ", signature='" + signature + '\'' + + ", signingKeyVersion=" + signingKeyVersion + + ", documentVersion=" + documentVersion + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SignedIdentityDocument that = (SignedIdentityDocument) o; + return signingKeyVersion == that.signingKeyVersion && + documentVersion == that.documentVersion && + Objects.equals(rawIdentityDocument, that.rawIdentityDocument) && + Objects.equals(identityDocument, that.identityDocument) && + Objects.equals(signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(rawIdentityDocument, identityDocument, signature, signingKeyVersion, documentVersion); + } +} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java new file mode 100644 index 00000000000..3024d1e0115 --- /dev/null +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author bjorncs + */ +@ExportPackage +package com.yahoo.vespa.hosted.athenz.instanceproviderservice; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def new file mode 100644 index 00000000000..3a2ef9c3092 --- /dev/null +++ b/athenz-identity-provider-service/src/main/resources/configdefinitions/athenz-provider-service.def @@ -0,0 +1,29 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=vespa.hosted.athenz.identityproviderservice.config + +# Athenz domain +domain string + +# Athenz service name +serviceName string + +# Current key version +keyVersion int default=0 + +# HTTPS port for Athenz Provider Service endpoint +port int default=8443 + +# File name prefix for private and public key. Component assumes suffix .[priv|pub].<version>. +keyPathPrefix string + +# InstanceConfirmation API path +apiPath string default="/athenz/v1/provider/instance" + +# Athenz principal authority header name +athenzPrincipalHeaderName string default="Athenz-Principal-Auth" + +# Athenz ZTS server url +ztsUrl string + +# Certificate DNS suffix +certDnsSuffix string diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java new file mode 100644 index 00000000000..3798a1e5496 --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzInstanceProviderServiceTest.java @@ -0,0 +1,163 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.athenz.auth.util.Crypto; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.hosted.athenz.identityproviderservice.config.AthenzProviderServiceConfig; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.KeyProvider; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.IdentityDocument; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.InstanceConfirmation; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.ProviderUniqueId; +import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.model.SignedIdentityDocument; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.junit.Ignore; +import org.junit.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.time.Instant; +import java.util.Base64; +import java.util.logging.Logger; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author bjorncs + */ +public class AthenzInstanceProviderServiceTest { + + private static final Logger log = Logger.getLogger(AthenzInstanceProviderServiceTest.class.getName()); + private static final int PORT = 12345; + + @Test + @Ignore("Requires private key for Athenz service") + public void provider_service_hosts_endpoint_secured_with_tls() throws Exception { + String domain = "vespa.vespa.cd"; + String service = "provider_dev_cd-us-central-1"; + DummyKeyProvider keyProvider = new DummyKeyProvider(); + PrivateKey privateKey = Crypto.loadPrivateKey(keyProvider.getPrivateKey(0)); + + AthenzProviderServiceConfig config = + new AthenzProviderServiceConfig( + new AthenzProviderServiceConfig.Builder() + .domain(domain) + .serviceName(service) + .port(PORT) + .keyPathPrefix("dummy-path") + .certDnsSuffix("INSERT DNS SUFFIX HERE") + .ztsUrl("INSERT ZTS URL HERE") + .athenzPrincipalHeaderName("INSERT PRINCIPAL HEADER NAME HERE") + .apiPath("/")); + + ScheduledExecutorServiceMock executor = new ScheduledExecutorServiceMock(); + AthenzInstanceProviderService athenzInstanceProviderService = new AthenzInstanceProviderService(config, keyProvider, executor); + + try (CloseableHttpClient client = createHttpClient(domain, service)) { + Runnable certificateRefreshCommand = executor.getCommand().orElseThrow(() -> new AssertionError("Command not present")); + assertFalse(getStatus(client)); + certificateRefreshCommand.run(); + assertTrue(getStatus(client)); + assertInstanceConfirmationSucceeds(client, privateKey); + certificateRefreshCommand.run(); + assertTrue(getStatus(client)); + assertInstanceConfirmationSucceeds(client, privateKey); + } finally { + athenzInstanceProviderService.deconstruct(); + } + } + + private static boolean getStatus(HttpClient client) { + try { + HttpResponse response = client.execute(new HttpGet("https://localhost:" + PORT + "/status.html")); + return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK; + } catch (Exception e) { + log.log(LogLevel.INFO, "Status.html failed: " + e); + return false; + } + } + + private static void assertInstanceConfirmationSucceeds(HttpClient client, PrivateKey privateKey) throws IOException { + HttpPost httpPost = new HttpPost("https://localhost:" + PORT + "/"); + httpPost.setEntity(createInstanceConfirmation(privateKey)); + HttpResponse response = client.execute(httpPost); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + } + + private static CloseableHttpClient createHttpClient(String domain, String service) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { + SSLContext sslContext = new SSLContextBuilder() + .loadTrustMaterial(null, (certificateChain, ignoredAuthType) -> + certificateChain[0].getSubjectX500Principal().getName().equals("CN=" + domain + "." + service)) + .build(); + + return HttpClients.custom() + .setSslcontext(sslContext) + .setSSLHostnameVerifier(new NoopHostnameVerifier()) + .build(); + } + + private static HttpEntity createInstanceConfirmation(PrivateKey privateKey) { + IdentityDocument identityDocument = new IdentityDocument( + "domain", "service", + new ProviderUniqueId( + "tenant", "application", "environment", "region", "instance", "cluster-id", 0), + "hostname", "instance-hostname", Instant.now()); + try { + ObjectMapper mapper = Utils.getMapper(); + String encodedIdentityDocument = + Base64.getEncoder().encodeToString(mapper.writeValueAsString(identityDocument).getBytes()); + Signature sigGenerator = Signature.getInstance("SHA512withRSA"); + sigGenerator.initSign(privateKey); + sigGenerator.update(encodedIdentityDocument.getBytes()); + String signature = Base64.getEncoder().encodeToString(sigGenerator.sign()); + + InstanceConfirmation instanceConfirmation = new InstanceConfirmation( + "provider", "domain", "service", + new SignedIdentityDocument(encodedIdentityDocument, signature, 0, 1)); + return new StringEntity(mapper.writeValueAsString(instanceConfirmation)); + } catch (JsonProcessingException + | NoSuchAlgorithmException + | UnsupportedEncodingException + | SignatureException + | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + private static class DummyKeyProvider implements KeyProvider { + + @Override + public String getPrivateKey(int version) { + return "INSERT PRIV KEY"; + } + + @Override + public String getPublicKey(int version) { + return "INSERT PUB KEY"; + } + } +} diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java new file mode 100644 index 00000000000..45cb82a0c0a --- /dev/null +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ScheduledExecutorServiceMock.java @@ -0,0 +1,115 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.athenz.instanceproviderservice; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author bjorncs + */ +public class ScheduledExecutorServiceMock implements ScheduledExecutorService { + + private Runnable runnable; + + public Optional<Runnable> getCommand() { + return Optional.ofNullable(runnable); + } + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + if (runnable != null) { + throw new IllegalStateException("Can only register single command"); + } + runnable = Objects.requireNonNull(command); + return null; + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + // do nothing + } + + @Override + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + public Future<?> submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Runnable command) { + throw new UnsupportedOperationException(); + } +} |