diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java index ae1370d2b5..d5fb07e629 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java @@ -180,6 +180,42 @@ private Options buildBaseOptions() { .build() ); + options.addOption( + Option.builder("keygenvaultapprole") + .desc("AppRole path for Hashicorp Vault authentication (defaults to 'approle')") + .hasArg() + .optionalArg(false) + .argName("STRING") + .build() + ); + + options.addOption( + Option.builder("keygenvaultkeystore") + .desc("Path to JKS keystore for TLS Hashicorp Vault communication") + .hasArg() + .optionalArg(false) + .argName("PATH") + .build() + ); + + options.addOption( + Option.builder("keygenvaulttruststore") + .desc("Path to JKS truststore for TLS Hashicorp Vault communication") + .hasArg() + .optionalArg(false) + .argName("PATH") + .build() + ); + + options.addOption( + Option.builder("keygenvaultsecretengine") + .desc("Name of already enabled Hashicorp v2 kv secret engine") + .hasArg() + .optionalArg(false) + .argName("STRING") + .build() + ); + options.addOption( Option.builder("pidfile") .desc("Path to pid file") diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java index a69726f9b7..730b497408 100644 --- a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java +++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParser.java @@ -1,14 +1,12 @@ package com.quorum.tessera.config.cli.parsers; -import com.quorum.tessera.config.ArgonOptions; -import com.quorum.tessera.config.AzureKeyVaultConfig; -import com.quorum.tessera.config.KeyVaultConfig; -import com.quorum.tessera.config.KeyVaultType; +import com.quorum.tessera.config.*; import com.quorum.tessera.config.cli.CliException; import com.quorum.tessera.config.keypairs.ConfigKeyPair; import com.quorum.tessera.config.util.JaxbUtil; import com.quorum.tessera.key.generation.KeyGenerator; import com.quorum.tessera.key.generation.KeyGeneratorFactory; +import com.quorum.tessera.key.generation.KeyVaultOptions; import org.apache.commons.cli.CommandLine; import javax.validation.ConstraintViolation; @@ -18,6 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -41,6 +40,7 @@ public class KeyGenerationParser implements Parser> { public List parse(final CommandLine commandLine) throws IOException { final ArgonOptions argonOptions = this.argonOptions(commandLine).orElse(null); + final KeyVaultOptions keyVaultOptions = this.keyVaultOptions(commandLine).orElse(null); final KeyVaultConfig keyVaultConfig = this.keyVaultConfig(commandLine).orElse(null); final KeyGenerator generator = factory.create(keyVaultConfig); @@ -48,7 +48,7 @@ public List parse(final CommandLine commandLine) throws IOExcepti if (commandLine.hasOption("keygen")) { return this.filenames(commandLine) .stream() - .map(name -> generator.generate(name, argonOptions)) + .map(name -> generator.generate(name, argonOptions, keyVaultOptions)) .collect(Collectors.toList()); } @@ -68,6 +68,12 @@ private Optional argonOptions(final CommandLine commandLine) throw return Optional.empty(); } + private Optional keyVaultOptions(final CommandLine commandLine) { + Optional secretEngineName = Optional.ofNullable(commandLine.getOptionValue("keygenvaultsecretengine")); + + return secretEngineName.map(KeyVaultOptions::new); + } + private List filenames(final CommandLine commandLine) { if (commandLine.hasOption("filename")) { @@ -88,23 +94,55 @@ private Optional keyVaultConfig(CommandLine commandLine) { return Optional.empty(); } - String t = commandLine.getOptionValue("keygenvaulttype"); + final String t = commandLine.getOptionValue("keygenvaulttype"); + KeyVaultType keyVaultType; try { - KeyVaultType.valueOf(t); + keyVaultType = KeyVaultType.valueOf( + t.trim() + .toUpperCase() + ); } catch(IllegalArgumentException | NullPointerException e) { - throw new CliException("Key vault type either not provided or not recognised. Ensure provided value is UPPERCASE and has no leading or trailing whitespace characters"); + throw new CliException("Key vault type either not provided or not recognised"); } String keyVaultUrl = commandLine.getOptionValue("keygenvaulturl"); - //Only Azure supported atm so no need to check keyvaulttype - KeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig(keyVaultUrl); + KeyVaultConfig keyVaultConfig; + + if(keyVaultType.equals(KeyVaultType.AZURE)) { + keyVaultConfig = new AzureKeyVaultConfig(keyVaultUrl); - Set> violations = validator.validate((AzureKeyVaultConfig)keyVaultConfig); + Set> violations = validator.validate((AzureKeyVaultConfig)keyVaultConfig); + + if(!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } else { + if(!commandLine.hasOption("filename")) { + throw new CliException("At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); + } - if(!violations.isEmpty()) { - throw new ConstraintViolationException(violations); + String approlePath = commandLine.getOptionValue("keygenvaultapprole"); + + Optional tlsKeyStorePath = Optional.ofNullable(commandLine.getOptionValue("keygenvaultkeystore")) + .map(Paths::get); + + Optional tlsTrustStorePath = Optional.ofNullable(commandLine.getOptionValue("keygenvaulttruststore")) + .map(Paths::get); + + keyVaultConfig = new HashicorpKeyVaultConfig( + keyVaultUrl, + approlePath, + tlsKeyStorePath.orElse(null), + tlsTrustStorePath.orElse(null) + ); + + Set> violations = validator.validate((HashicorpKeyVaultConfig)keyVaultConfig); + + if(!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } } return Optional.of(keyVaultConfig); diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java index 9992ecf1a7..765a835071 100644 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java @@ -122,7 +122,7 @@ public void keygenWithConfig() throws Exception { Files.write(publicKeyPath, Arrays.asList("SOMEDATA")); FilesystemKeyPair keypair = new FilesystemKeyPair(publicKeyPath, privateKeyPath); - when(keyGenerator.generate(anyString(), eq(null))).thenReturn(keypair); + when(keyGenerator.generate(anyString(), eq(null), eq(null))).thenReturn(keypair); Path unixSocketPath = Files.createTempFile(UUID.randomUUID().toString(), ".ipc"); @@ -143,7 +143,7 @@ public void keygenWithConfig() throws Exception { assertThat(result.getConfig()).isNotNull(); assertThat(result.isSuppressStartup()).isFalse(); - verify(keyGenerator).generate(anyString(), eq(null)); + verify(keyGenerator).generate(anyString(), eq(null), eq(null)); verifyNoMoreInteractions(keyGenerator); } @@ -191,7 +191,7 @@ public void output() throws Exception { Files.write(publicKeyPath, Arrays.asList("SOMEDATA")); FilesystemKeyPair keypair = new FilesystemKeyPair(publicKeyPath, privateKeyPath); - when(keyGenerator.generate(anyString(), eq(null))).thenReturn(keypair); + when(keyGenerator.generate(anyString(), eq(null), eq(null))).thenReturn(keypair); Path generatedKey = Paths.get("/tmp/" + UUID.randomUUID().toString()); @@ -376,7 +376,7 @@ public void allowStartupForKeygenAndConfigfileOptions() throws Exception { Files.write(publicKeyPath, Arrays.asList("SOMEDATA")); FilesystemKeyPair keypair = new FilesystemKeyPair(publicKeyPath, privateKeyPath); - when(keyGenerator.generate(anyString(), eq(null))).thenReturn(keypair); + when(keyGenerator.generate(anyString(), eq(null), eq(null))).thenReturn(keypair); final Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); @@ -389,7 +389,7 @@ public void allowStartupForKeygenAndConfigfileOptions() throws Exception { public void suppressStartupForKeygenAndVaultUrlAndConfigfileOptions() throws Exception { final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); final FilesystemKeyPair keypair = new FilesystemKeyPair(Paths.get(""), Paths.get("")); - when(keyGenerator.generate(anyString(), eq(null))).thenReturn(keypair); + when(keyGenerator.generate(anyString(), eq(null), eq(null))).thenReturn(keypair); final Path configFile = createAndPopulatePaths(getClass().getResource("/sample-config.json")); final String vaultUrl = "https://test.vault.azure.net"; diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java index 3e315c51e4..05bbd4f812 100644 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/OverrideUtilTest.java @@ -1,6 +1,9 @@ package com.quorum.tessera.config.cli; -import com.quorum.tessera.config.*; +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.KeyConfiguration; +import com.quorum.tessera.config.Peer; +import com.quorum.tessera.config.SslAuthenticationMode; import com.quorum.tessera.config.util.JaxbUtil; import org.junit.Ignore; import org.junit.Test; @@ -29,80 +32,84 @@ public class OverrideUtilTest { public void buildOptions() { final List expected = Arrays.asList( - "jdbc.username", - "jdbc.password", - "jdbc.url", - "jdbc.autoCreateTables", - "peer.url", - "keys.passwordFile", - "keys.passwords", - "keys.keyData.config.data.aopts.algorithm", - "keys.keyData.config.data.aopts.iterations", - "keys.keyData.config.data.aopts.memory", - "keys.keyData.config.data.aopts.parallelism", - "keys.keyData.privateKeyPath", - "keys.azureKeyVaultConfig.url", - "alwaysSendTo", - "unixSocketFile", - "useWhiteList", - "disablePeerDiscovery", - "serverConfigs.sslConfig.serverTrustStore", - "serverConfigs.influxConfig.dbName", - "serverConfigs.sslConfig.knownClientsFile", - "serverConfigs.influxConfig.hostName", - "serverConfigs.sslConfig.serverTrustCertificates", - "serverConfigs.sslConfig.clientTrustCertificates", - "serverConfigs.sslConfig.clientTrustStorePassword", - "serverConfigs.sslConfig.generateKeyStoreIfNotExisted", - "serverConfigs.influxConfig.pushIntervalInSecs", - "serverConfigs.bindingAddress", - "serverConfigs.sslConfig.serverKeyStore", - "serverConfigs.sslConfig.serverTrustStorePassword", - "serverConfigs.sslConfig.serverKeyStorePassword", - "serverConfigs.sslConfig.clientTrustMode", - "serverConfigs.sslConfig.clientKeyStorePassword", - "serverConfigs.communicationType", - "serverConfigs.sslConfig.clientTlsCertificatePath", - "serverConfigs.sslConfig.serverTlsKeyPath", - "serverConfigs.sslConfig.clientKeyStore", - "serverConfigs.sslConfig.serverTrustMode", - "serverConfigs.influxConfig.port", - "serverConfigs.sslConfig.clientTlsKeyPath", - "serverConfigs.app", - "serverConfigs.sslConfig.clientTrustStore", - "serverConfigs.enabled", - "serverConfigs.sslConfig.serverTlsCertificatePath", - "serverConfigs.sslConfig.tls", - "serverConfigs.sslConfig.knownServersFile", - "server.hostName", - "server.sslConfig.knownServersFile", - "server.sslConfig.clientTrustStorePassword", - "server.sslConfig.clientKeyStorePassword", - "server.sslConfig.clientTlsKeyPath", - "server.sslConfig.clientTrustCertificates", - "server.sslConfig.knownClientsFile", - "server.communicationType", - "server.sslConfig.serverTrustStorePassword", - "server.sslConfig.serverTrustCertificates", - "server.sslConfig.clientTrustStore", - "server.sslConfig.tls", - "server.sslConfig.serverTlsCertificatePath", - "server.grpcPort", - "server.sslConfig.serverKeyStore", - "server.influxConfig.port", - "server.port", - "server.sslConfig.generateKeyStoreIfNotExisted", - "server.sslConfig.clientTlsCertificatePath", - "server.sslConfig.serverTlsKeyPath", - "server.influxConfig.hostName", - "server.sslConfig.serverTrustStore", - "server.bindingAddress", - "server.sslConfig.serverTrustMode", - "server.sslConfig.clientKeyStore", - "server.influxConfig.dbName", - "server.sslConfig.clientTrustMode", - "server.influxConfig.pushIntervalInSecs", - "server.sslConfig.serverKeyStorePassword" + "jdbc.username", + "jdbc.password", + "jdbc.url", + "jdbc.autoCreateTables", + "peer.url", + "keys.passwordFile", + "keys.passwords", + "keys.keyData.config.data.aopts.algorithm", + "keys.keyData.config.data.aopts.iterations", + "keys.keyData.config.data.aopts.memory", + "keys.keyData.config.data.aopts.parallelism", + "keys.keyData.privateKeyPath", + "keys.azureKeyVaultConfig.url", + "keys.hashicorpKeyVaultConfig.approlePath", + "keys.hashicorpKeyVaultConfig.tlsKeyStorePath", + "keys.hashicorpKeyVaultConfig.tlsTrustStorePath", + "keys.hashicorpKeyVaultConfig.url", + "alwaysSendTo", + "unixSocketFile", + "useWhiteList", + "disablePeerDiscovery", + "serverConfigs.sslConfig.serverTrustStore", + "serverConfigs.influxConfig.dbName", + "serverConfigs.sslConfig.knownClientsFile", + "serverConfigs.influxConfig.hostName", + "serverConfigs.sslConfig.serverTrustCertificates", + "serverConfigs.sslConfig.clientTrustCertificates", + "serverConfigs.sslConfig.clientTrustStorePassword", + "serverConfigs.sslConfig.generateKeyStoreIfNotExisted", + "serverConfigs.influxConfig.pushIntervalInSecs", + "serverConfigs.bindingAddress", + "serverConfigs.sslConfig.serverKeyStore", + "serverConfigs.sslConfig.serverTrustStorePassword", + "serverConfigs.sslConfig.serverKeyStorePassword", + "serverConfigs.sslConfig.clientTrustMode", + "serverConfigs.sslConfig.clientKeyStorePassword", + "serverConfigs.communicationType", + "serverConfigs.sslConfig.clientTlsCertificatePath", + "serverConfigs.sslConfig.serverTlsKeyPath", + "serverConfigs.sslConfig.clientKeyStore", + "serverConfigs.sslConfig.serverTrustMode", + "serverConfigs.influxConfig.port", + "serverConfigs.sslConfig.clientTlsKeyPath", + "serverConfigs.app", + "serverConfigs.sslConfig.clientTrustStore", + "serverConfigs.enabled", + "serverConfigs.sslConfig.serverTlsCertificatePath", + "serverConfigs.sslConfig.tls", + "serverConfigs.sslConfig.knownServersFile", + "server.hostName", + "server.sslConfig.knownServersFile", + "server.sslConfig.clientTrustStorePassword", + "server.sslConfig.clientKeyStorePassword", + "server.sslConfig.clientTlsKeyPath", + "server.sslConfig.clientTrustCertificates", + "server.sslConfig.knownClientsFile", + "server.communicationType", + "server.sslConfig.serverTrustStorePassword", + "server.sslConfig.serverTrustCertificates", + "server.sslConfig.clientTrustStore", + "server.sslConfig.tls", + "server.sslConfig.serverTlsCertificatePath", + "server.grpcPort", + "server.sslConfig.serverKeyStore", + "server.influxConfig.port", + "server.port", + "server.sslConfig.generateKeyStoreIfNotExisted", + "server.sslConfig.clientTlsCertificatePath", + "server.sslConfig.serverTlsKeyPath", + "server.influxConfig.hostName", + "server.sslConfig.serverTrustStore", + "server.bindingAddress", + "server.sslConfig.serverTrustMode", + "server.sslConfig.clientKeyStore", + "server.influxConfig.dbName", + "server.sslConfig.clientTrustMode", + "server.influxConfig.pushIntervalInSecs", + "server.sslConfig.serverKeyStorePassword" ); final Map results = OverrideUtil.buildConfigOptions(); diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java index 45f98bc2d0..f8171ffa85 100644 --- a/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java +++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/parsers/KeyGenerationParserTest.java @@ -13,6 +13,7 @@ import javax.validation.ConstraintViolationException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; @@ -44,7 +45,7 @@ public void notProvidingArgonOptionsGivesNull() throws Exception { final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); final ArgumentCaptor captor = ArgumentCaptor.forClass(ArgonOptions.class); - verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture()); + verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture(), eq(null)); assertThat(captor.getAllValues()).hasSize(1); assertThat(captor.getValue()).isNull(); @@ -71,7 +72,7 @@ public void providingArgonOptionsGetSentCorrectly() throws Exception { final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); final ArgumentCaptor captor = ArgumentCaptor.forClass(ArgonOptions.class); - verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture()); + verify(keyGenerator).generate(eq(keyLocation.toString()), captor.capture(), eq(null)); assertThat(captor.getAllValues()).hasSize(1); assertThat(captor.getValue().getAlgorithm()).isEqualTo("id"); @@ -92,7 +93,7 @@ public void keygenWithNoName() throws Exception { assertThat(result).isNotNull().hasSize(1); final KeyGenerator keyGenerator = MockKeyGeneratorFactory.getMockKeyGenerator(); - verify(keyGenerator).generate("", null); + verify(keyGenerator).generate("", null, null); } @Test @@ -135,7 +136,7 @@ public void ifAllVaultOptionsProvidedAndValidThenOkay() throws Exception { } @Test - public void ifOnlyValidVaultTypeOptionProvidedThenValidationException() { + public void ifAzureVaultTypeOptionProvidedButNoVaultUrlThenValidationException() { when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("AZURE"); @@ -157,6 +158,31 @@ public void ifOnlyValidVaultTypeOptionProvidedThenValidationException() { assertThat(violation.getMessage()).isEqualTo("may not be null"); } + @Test + public void ifHashicorpVaultTypeOptionAndFilenameProvidedButNoVaultUrlThenValidationException() { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(false); + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + + Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(1); + + ConstraintViolation violation = violations.iterator().next(); + + assertThat(violation.getPropertyPath().toString()).isEqualTo("url"); + assertThat(violation.getMessage()).isEqualTo("may not be null"); + } + @Test public void ifOnlyVaultUrlOptionProvidedThenException() { when(commandLine.hasOption("keygenvaulttype")).thenReturn(false); @@ -166,11 +192,11 @@ public void ifOnlyVaultUrlOptionProvidedThenException() { Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); assertThat(ex).isInstanceOf(CliException.class); - assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised. Ensure provided value is UPPERCASE and has no leading or trailing whitespace characters"); + assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised"); } @Test - public void ifAllVaultOptionsProvidedButTypeUnknownThenException() { + public void ifVaultOptionsProvidedButTypeUnknownThenException() { when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("unknown"); @@ -178,7 +204,166 @@ public void ifAllVaultOptionsProvidedButTypeUnknownThenException() { Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); assertThat(ex).isInstanceOf(CliException.class); - assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised. Ensure provided value is UPPERCASE and has no leading or trailing whitespace characters"); + assertThat(ex.getMessage()).isEqualTo("Key vault type either not provided or not recognised"); + } + + @Test + public void noFilenameProvidedWhenUsingHashicorpVaultThrowsException() { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); + when(commandLine.hasOption("filename")).thenReturn(false); + + Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); + + assertThat(ex).isInstanceOf(CliException.class); + assertThat(ex.getMessage()).isEqualTo("At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); + } + + @Test + public void ifAllVaultOptionsAndFilenameProvidedForHashicorpThenOkay() throws Exception { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); + when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); + + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); + when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); + + this.parser.parse(commandLine); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); + verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); + verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); + verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); } + @Test + public void ifHashicorpTlsOptionsProvidedButPathsDontExistThenValidationException() { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP"); + when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); + when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); + when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn("non/existent/path"); + when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn("non/existent/path"); + + Throwable ex = catchThrowable(() -> this.parser.parse(commandLine)); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); + verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); + verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); + verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(2); + + Iterator> iterator = violations.iterator(); + + assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); + assertThat(iterator.next().getMessage()).isEqualTo("File does not exist"); + } + + @Test + public void lowercaseVaultTypeIsOkay() throws Exception { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); + when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); + + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("hashicorp"); + + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); + when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); + + this.parser.parse(commandLine); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); + verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); + verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); + verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); + } + + @Test + public void leadingWhitespaceVaultTypeIsOkay() throws Exception { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); + when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); + + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn(" HASHICORP"); + + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); + when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); + + this.parser.parse(commandLine); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); + verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); + verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); + verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); + } + + @Test + public void trailingWhitespaceVaultTypeIsOkay() throws Exception { + when(commandLine.hasOption("keygenvaulttype")).thenReturn(true); + when(commandLine.hasOption("keygenvaulturl")).thenReturn(true); + when(commandLine.hasOption("filename")).thenReturn(true); + when(commandLine.getOptionValue("keygenvaulturl")).thenReturn("someurl"); + when(commandLine.getOptionValue("filename")).thenReturn("secret/path"); + when(commandLine.getOptionValue("keygenvaultapprole")).thenReturn("approle"); + when(commandLine.getOptionValue("keygenvaultsecretengine")).thenReturn("secretEngine"); + + when(commandLine.getOptionValue("keygenvaulttype")).thenReturn("HASHICORP "); + + Path tempPath = Files.createTempFile(UUID.randomUUID().toString(), ""); + tempPath.toFile().deleteOnExit(); + + when(commandLine.getOptionValue("keygenvaultkeystore")).thenReturn(tempPath.toString()); + when(commandLine.getOptionValue("keygenvaulttruststore")).thenReturn(tempPath.toString()); + + this.parser.parse(commandLine); + + verify(commandLine, times(1)).getOptionValue("keygenvaulttype"); + verify(commandLine, times(1)).getOptionValue("keygenvaulturl"); + verify(commandLine, times(1)).getOptionValue("keygenvaultapprole"); + verify(commandLine, times(1)).getOptionValue("keygenvaultkeystore"); + verify(commandLine, times(1)).getOptionValue("keygenvaulttruststore"); + verify(commandLine, times(1)).getOptionValue("keygenvaultsecretengine"); + } + + } diff --git a/config-migration/src/main/java/com/quorum/tessera/config/builder/KeyDataBuilder.java b/config-migration/src/main/java/com/quorum/tessera/config/builder/KeyDataBuilder.java index d995f1ecb9..6b798e0f99 100644 --- a/config-migration/src/main/java/com/quorum/tessera/config/builder/KeyDataBuilder.java +++ b/config-migration/src/main/java/com/quorum/tessera/config/builder/KeyDataBuilder.java @@ -70,7 +70,7 @@ public KeyConfiguration build() { privateKeyPasswordFilePath = null; } - return new KeyConfiguration(privateKeyPasswordFilePath, null, keyData, null); + return new KeyConfiguration(privateKeyPasswordFilePath, null, keyData, null, null); } } diff --git a/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java b/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java index d5f61f2158..b223035db7 100644 --- a/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java +++ b/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java @@ -158,7 +158,7 @@ public void ifPublicAndPrivateKeyListAreEmptyThenKeyConfigurationIsAllNulls() th KeyConfiguration result = tomlConfigFactory.createKeyDataBuilder(configData).build(); assertThat(result).isNotNull(); - KeyConfiguration expected = new KeyConfiguration(null, null, Collections.emptyList(), null); + KeyConfiguration expected = new KeyConfiguration(null, null, Collections.emptyList(), null, null); assertThat(result).isEqualTo(expected); } diff --git a/config-migration/src/test/java/com/quorum/tessera/config/migration/test/FixtureUtil.java b/config-migration/src/test/java/com/quorum/tessera/config/migration/test/FixtureUtil.java index 1e97c85138..5ce315d9ea 100644 --- a/config-migration/src/test/java/com/quorum/tessera/config/migration/test/FixtureUtil.java +++ b/config-migration/src/test/java/com/quorum/tessera/config/migration/test/FixtureUtil.java @@ -61,7 +61,7 @@ public static ConfigBuilder builderWithValidValues() { .sslClientTlsCertificatePath("sslClientTlsCertificatePath") .sslServerTlsCertificatePath("sslServerTlsCertificatePath") .keyData(new KeyConfiguration(null, Collections.emptyList(), - Collections.singletonList(new FilesystemKeyPair(Paths.get("public"), Paths.get("private"))), null)); + Collections.singletonList(new FilesystemKeyPair(Paths.get("public"), Paths.get("private"))), null, null)); } public static ConfigBuilder builderWithNullValues() { @@ -91,7 +91,7 @@ public static ConfigBuilder builderWithNullValues() { .sslClientTlsCertificatePath("sslClientTlsCertificatePath") .sslServerTlsCertificatePath("sslServerTlsCertificatePath") .keyData(new KeyConfiguration(null, Collections.emptyList(), - Collections.singletonList(new FilesystemKeyPair(Paths.get("public"), Paths.get("private"))), null)); + Collections.singletonList(new FilesystemKeyPair(Paths.get("public"), Paths.get("private"))), null, null)); } public static JsonObject createUnlockedPrivateKey() { diff --git a/config/src/main/java/com/quorum/tessera/config/HashicorpKeyVaultConfig.java b/config/src/main/java/com/quorum/tessera/config/HashicorpKeyVaultConfig.java new file mode 100644 index 0000000000..020a859087 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/HashicorpKeyVaultConfig.java @@ -0,0 +1,85 @@ +package com.quorum.tessera.config; + +import com.quorum.tessera.config.adapters.PathAdapter; +import com.quorum.tessera.config.constraints.ValidPath; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.nio.file.Path; + +public class HashicorpKeyVaultConfig extends ConfigItem implements KeyVaultConfig { + + @Valid + @NotNull + @XmlAttribute + private String url; + + @Valid + @XmlElement + private String approlePath; + + @Valid + @ValidPath(checkExists = true, message = "File does not exist") + @XmlElement(type = String.class) + @XmlJavaTypeAdapter(PathAdapter.class) + private Path tlsKeyStorePath; + + @Valid + @ValidPath(checkExists = true, message = "File does not exist") + @XmlElement(type = String.class) + @XmlJavaTypeAdapter(PathAdapter.class) + private Path tlsTrustStorePath; + + public HashicorpKeyVaultConfig(String url, String approlePath, Path tlsKeyStorePath, Path tlsTrustStorePath) { + this.url = url; + this.approlePath = approlePath; + this.tlsKeyStorePath = tlsKeyStorePath; + this.tlsTrustStorePath = tlsTrustStorePath; + } + + public HashicorpKeyVaultConfig() { + } + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Path getTlsKeyStorePath() { + return tlsKeyStorePath; + } + + public void setTlsKeyStorePath(Path tlsKeyStorePath) { + this.tlsKeyStorePath = tlsKeyStorePath; + } + + public Path getTlsTrustStorePath() { + return tlsTrustStorePath; + } + + public void setTlsTrustStorePath(Path tlsTrustStorePath) { + this.tlsTrustStorePath = tlsTrustStorePath; + } + + public String getApprolePath() { + if(approlePath == null) { + return "approle"; + } + return approlePath; + } + + public void setApprolePath(String approlePath) { + this.approlePath = approlePath; + } + + @Override + public KeyVaultType getKeyVaultType() { + return KeyVaultType.HASHICORP; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/JaxbConfigFactory.java b/config/src/main/java/com/quorum/tessera/config/JaxbConfigFactory.java index ebf265133a..862a193cf2 100644 --- a/config/src/main/java/com/quorum/tessera/config/JaxbConfigFactory.java +++ b/config/src/main/java/com/quorum/tessera/config/JaxbConfigFactory.java @@ -69,7 +69,7 @@ public Config create(final InputStream configData, final List new config.getJdbcConfig(), config.getServerConfigs(), config.getPeers(), - new KeyConfiguration(Paths.get("passwords.txt"), null, config.getKeys().getKeyData(), config.getKeys().getAzureKeyVaultConfig()), + new KeyConfiguration(Paths.get("passwords.txt"), null, config.getKeys().getKeyData(), config.getKeys().getAzureKeyVaultConfig(), config.getKeys().getHashicorpKeyVaultConfig()), config.getAlwaysSendTo(), config.getUnixSocketFile(), config.isUseWhiteList(), diff --git a/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java b/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java index 59d9a08e33..2875ff0808 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java @@ -35,11 +35,16 @@ public class KeyConfiguration extends ConfigItem { @XmlElement private AzureKeyVaultConfig azureKeyVaultConfig; - public KeyConfiguration(final Path passwordFile, final List passwords, final List keyData, final AzureKeyVaultConfig azureKeyVaultConfig) { + @Valid + @XmlElement + private HashicorpKeyVaultConfig hashicorpKeyVaultConfig; + + public KeyConfiguration(final Path passwordFile, final List passwords, final List keyData, final AzureKeyVaultConfig azureKeyVaultConfig, final HashicorpKeyVaultConfig hashicorpKeyVaultConfig) { this.passwordFile = passwordFile; this.passwords = passwords; this.keyData = keyData; this.azureKeyVaultConfig = azureKeyVaultConfig; + this.hashicorpKeyVaultConfig = hashicorpKeyVaultConfig; } public KeyConfiguration() { @@ -61,6 +66,10 @@ public AzureKeyVaultConfig getAzureKeyVaultConfig() { return this.azureKeyVaultConfig; } + public HashicorpKeyVaultConfig getHashicorpKeyVaultConfig() { + return hashicorpKeyVaultConfig; + } + public void setPasswordFile(Path passwordFile) { this.passwordFile = passwordFile; } @@ -77,5 +86,8 @@ public void setAzureKeyVaultConfig(AzureKeyVaultConfig azureKeyVaultConfig) { this.azureKeyVaultConfig = azureKeyVaultConfig; } + public void setHashicorpKeyVaultConfig(HashicorpKeyVaultConfig hashicorpKeyVaultConfig) { + this.hashicorpKeyVaultConfig = hashicorpKeyVaultConfig; + } } diff --git a/config/src/main/java/com/quorum/tessera/config/KeyData.java b/config/src/main/java/com/quorum/tessera/config/KeyData.java index f0d5516827..b48b1823c5 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyData.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyData.java @@ -43,20 +43,34 @@ public class KeyData extends ConfigItem { @Pattern(regexp = "^[0-9a-zA-Z\\-]*$") private String azureVaultPrivateKeyId; - public KeyData(final KeyDataConfig keyDataConfig, - final String privateKey, - final String publicKey, - final Path privKeyPath, - final Path pubKeyPath, - final String azureVaultPrivateKeyId, - final String azureVaultPublicKeyId) { + @XmlElement + private String hashicorpVaultPublicKeyId; + + @XmlElement + private String hashicorpVaultPrivateKeyId; + + @XmlElement + private String hashicorpVaultSecretEngineName; + + @XmlElement + private String hashicorpVaultSecretName; + + @XmlElement + private String hashicorpVaultSecretVersion; + + public KeyData(KeyDataConfig config, String privateKey, String publicKey, Path privateKeyPath, Path publicKeyPath, String azureVaultPublicKeyId, String azureVaultPrivateKeyId, String hashicorpVaultPublicKeyId, String hashicorpVaultPrivateKeyId, String hashicorpVaultSecretEngineName, String hashicorpVaultSecretName, String hashicorpVaultSecretVersion) { + this.config = config; this.privateKey = privateKey; this.publicKey = publicKey; - this.config = keyDataConfig; - this.privateKeyPath = privKeyPath; - this.publicKeyPath = pubKeyPath; + this.privateKeyPath = privateKeyPath; + this.publicKeyPath = publicKeyPath; this.azureVaultPublicKeyId = azureVaultPublicKeyId; this.azureVaultPrivateKeyId = azureVaultPrivateKeyId; + this.hashicorpVaultPublicKeyId = hashicorpVaultPublicKeyId; + this.hashicorpVaultPrivateKeyId = hashicorpVaultPrivateKeyId; + this.hashicorpVaultSecretEngineName = hashicorpVaultSecretEngineName; + this.hashicorpVaultSecretName = hashicorpVaultSecretName; + this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; } public KeyData() { @@ -91,4 +105,71 @@ public String getAzureVaultPrivateKeyId() { return azureVaultPrivateKeyId; } + public String getHashicorpVaultPublicKeyId() { + return hashicorpVaultPublicKeyId; + } + + public String getHashicorpVaultPrivateKeyId() { + return hashicorpVaultPrivateKeyId; + } + + public String getHashicorpVaultSecretEngineName() { + return hashicorpVaultSecretEngineName; + } + + public String getHashicorpVaultSecretName() { + return hashicorpVaultSecretName; + } + + public String getHashicorpVaultSecretVersion() { + return hashicorpVaultSecretVersion; + } + + public void setConfig(KeyDataConfig config) { + this.config = config; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public void setPrivateKeyPath(Path privateKeyPath) { + this.privateKeyPath = privateKeyPath; + } + + public void setPublicKeyPath(Path publicKeyPath) { + this.publicKeyPath = publicKeyPath; + } + + public void setAzureVaultPublicKeyId(String azureVaultPublicKeyId) { + this.azureVaultPublicKeyId = azureVaultPublicKeyId; + } + + public void setAzureVaultPrivateKeyId(String azureVaultPrivateKeyId) { + this.azureVaultPrivateKeyId = azureVaultPrivateKeyId; + } + + public void setHashicorpVaultPublicKeyId(String hashicorpVaultPublicKeyId) { + this.hashicorpVaultPublicKeyId = hashicorpVaultPublicKeyId; + } + + public void setHashicorpVaultPrivateKeyId(String hashicorpVaultPrivateKeyId) { + this.hashicorpVaultPrivateKeyId = hashicorpVaultPrivateKeyId; + } + + public void setHashicorpVaultSecretEngineName(String hashicorpVaultSecretEngineName) { + this.hashicorpVaultSecretEngineName = hashicorpVaultSecretEngineName; + } + + public void setHashicorpVaultSecretName(String hashicorpVaultSecretName) { + this.hashicorpVaultSecretName = hashicorpVaultSecretName; + } + + public void setHashicorpVaultSecretVersion(String hashicorpVaultSecretVersion) { + this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; + } } diff --git a/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java b/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java index 2fae03681f..fa0dfd6e07 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java @@ -1,5 +1,5 @@ package com.quorum.tessera.config; public enum KeyVaultType { - AZURE + AZURE, HASHICORP } diff --git a/config/src/main/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapter.java b/config/src/main/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapter.java index 36c9a71178..dfff62c5cd 100644 --- a/config/src/main/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapter.java +++ b/config/src/main/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapter.java @@ -47,7 +47,7 @@ public KeyConfiguration unmarshal(final KeyConfiguration input) { }).collect(Collectors.toList()); } - return new KeyConfiguration(input.getPasswordFile(), input.getPasswords(), keyDataWithPasswords, input.getAzureKeyVaultConfig()); + return new KeyConfiguration(input.getPasswordFile(), input.getPasswords(), keyDataWithPasswords, input.getAzureKeyVaultConfig(), input.getHashicorpKeyVaultConfig()); } @Override diff --git a/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java b/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java index 5f2f82f7fd..02ee470e33 100644 --- a/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java +++ b/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java @@ -23,17 +23,23 @@ public ConfigKeyPair unmarshal(final KeyData keyData) { return new InlineKeypair(keyData.getPublicKey(), keyData.getConfig()); } - //case 3, the key vault ids are provided + //case 3, the Azure Key Vault data is provided if(keyData.getAzureVaultPublicKeyId() != null && keyData.getAzureVaultPrivateKeyId() != null) { return new AzureVaultKeyPair(keyData.getAzureVaultPublicKeyId(), keyData.getAzureVaultPrivateKeyId()); } - //case 4, the keys are provided inside a file + //case 4, the Hashicorp Vault data is provided + if(keyData.getHashicorpVaultPublicKeyId() != null && keyData.getHashicorpVaultPrivateKeyId() != null + && keyData.getHashicorpVaultSecretEngineName() != null && keyData.getHashicorpVaultSecretName() != null) { + return new HashicorpVaultKeyPair(keyData.getHashicorpVaultPublicKeyId(), keyData.getHashicorpVaultPrivateKeyId(), keyData.getHashicorpVaultSecretEngineName(), keyData.getHashicorpVaultSecretName(), keyData.getHashicorpVaultSecretVersion()); + } + + //case 5, the keys are provided inside a file if(keyData.getPublicKeyPath() != null && keyData.getPrivateKeyPath() != null) { return new FilesystemKeyPair(keyData.getPublicKeyPath(), keyData.getPrivateKeyPath()); } - //case 5, the key config specified is invalid + //case 6, the key config specified is invalid return new UnsupportedKeyPair( keyData.getConfig(), keyData.getPrivateKey(), @@ -41,38 +47,79 @@ public ConfigKeyPair unmarshal(final KeyData keyData) { keyData.getPrivateKeyPath(), keyData.getPublicKeyPath(), keyData.getAzureVaultPublicKeyId(), - keyData.getAzureVaultPrivateKeyId() + keyData.getAzureVaultPrivateKeyId(), + keyData.getHashicorpVaultPublicKeyId(), + keyData.getHashicorpVaultPrivateKeyId(), + keyData.getHashicorpVaultSecretEngineName(), + keyData.getHashicorpVaultSecretName(), + keyData.getHashicorpVaultSecretVersion() ); } @Override - public KeyData marshal(final ConfigKeyPair keyData) { + public KeyData marshal(final ConfigKeyPair keyPair) { + + KeyData keyData = new KeyData(); + + if(keyPair instanceof DirectKeyPair) { + DirectKeyPair kp = (DirectKeyPair) keyPair; + + keyData.setPublicKey(kp.getPublicKey()); + keyData.setPrivateKey(kp.getPrivateKey()); + return keyData; + } + + if(keyPair instanceof InlineKeypair) { + InlineKeypair kp = (InlineKeypair) keyPair; - if(keyData instanceof DirectKeyPair) { - DirectKeyPair kp = (DirectKeyPair) keyData; - return new KeyData(null, kp.getPrivateKey(), kp.getPublicKey(), null, null, null, null); + keyData.setPublicKey(kp.getPublicKey()); + keyData.setConfig(kp.getPrivateKeyConfig()); + return keyData; } - if(keyData instanceof InlineKeypair) { - InlineKeypair kp = (InlineKeypair) keyData; - return new KeyData(kp.getPrivateKeyConfig(), null, kp.getPublicKey(), null, null, null, null); + if(keyPair instanceof AzureVaultKeyPair) { + AzureVaultKeyPair kp = (AzureVaultKeyPair) keyPair; + + keyData.setAzureVaultPublicKeyId(kp.getPublicKeyId()); + keyData.setAzureVaultPrivateKeyId(kp.getPrivateKeyId()); + return keyData; } - if(keyData instanceof AzureVaultKeyPair) { - AzureVaultKeyPair kp = (AzureVaultKeyPair) keyData; - return new KeyData(null, null, null, null, null, kp.getPrivateKeyId(), kp.getPublicKeyId()); + if(keyPair instanceof HashicorpVaultKeyPair) { + HashicorpVaultKeyPair kp = (HashicorpVaultKeyPair) keyPair; + + keyData.setHashicorpVaultPublicKeyId(kp.getPublicKeyId()); + keyData.setHashicorpVaultPrivateKeyId(kp.getPrivateKeyId()); + keyData.setHashicorpVaultSecretEngineName(kp.getSecretEngineName()); + keyData.setHashicorpVaultSecretName(kp.getSecretName()); + return keyData; } - if(keyData instanceof FilesystemKeyPair) { - FilesystemKeyPair kp = (FilesystemKeyPair) keyData; - return new KeyData(null, null, null, kp.getPrivateKeyPath(), kp.getPublicKeyPath(), null, null); + if(keyPair instanceof FilesystemKeyPair) { + FilesystemKeyPair kp = (FilesystemKeyPair) keyPair; + + keyData.setPublicKeyPath(kp.getPublicKeyPath()); + keyData.setPrivateKeyPath(kp.getPrivateKeyPath()); + return keyData; } - if(keyData instanceof UnsupportedKeyPair) { - UnsupportedKeyPair kp = (UnsupportedKeyPair) keyData; - return new KeyData(kp.getConfig(), kp.getPrivateKey(), kp.getPublicKey(), kp.getPrivateKeyPath(), kp.getPublicKeyPath(), kp.getAzureVaultPrivateKeyId(), kp.getAzureVaultPublicKeyId()); + if(keyPair instanceof UnsupportedKeyPair) { + UnsupportedKeyPair kp = (UnsupportedKeyPair) keyPair; + return new KeyData( + kp.getConfig(), + kp.getPrivateKey(), + kp.getPublicKey(), + kp.getPrivateKeyPath(), + kp.getPublicKeyPath(), + kp.getAzureVaultPrivateKeyId(), + kp.getAzureVaultPublicKeyId(), + kp.getHashicorpVaultPrivateKeyId(), + kp.getHashicorpVaultPublicKeyId(), + kp.getHashicorpVaultSecretEngineName(), + kp.getHashicorpVaultSecretName(), + kp.getHashicorpVaultSecretVersion()); } - throw new UnsupportedOperationException("The keypair type " + keyData.getClass() + " is not allowed"); + throw new UnsupportedOperationException("The keypair type " + keyPair.getClass() + " is not allowed"); } } diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidator.java index 1edd7d9cf8..424a7eb277 100644 --- a/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidator.java +++ b/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidator.java @@ -2,6 +2,7 @@ import com.quorum.tessera.config.KeyConfiguration; import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; +import com.quorum.tessera.config.keypairs.HashicorpVaultKeyPair; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; @@ -28,6 +29,22 @@ public boolean isValid(KeyConfiguration keyConfiguration, ConstraintValidatorCon .anyMatch(keyPair -> keyPair instanceof AzureVaultKeyPair); if(isUsingAzureVaultKeys && keyConfiguration.getAzureKeyVaultConfig() == null) { + cvc.disableDefaultConstraintViolation(); + cvc.buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.azure.message}") + .addConstraintViolation(); + + return false; + } + + boolean isUsingHashicorpVaultKeys = keyConfiguration.getKeyData() + .stream() + .anyMatch(keyPair -> keyPair instanceof HashicorpVaultKeyPair); + + if(isUsingHashicorpVaultKeys && keyConfiguration.getHashicorpKeyVaultConfig() == null) { + cvc.disableDefaultConstraintViolation(); + cvc.buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.hashicorp.message}") + .addConstraintViolation(); + return false; } diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/PositiveIntegerValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/PositiveIntegerValidator.java new file mode 100644 index 0000000000..de99aec063 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/constraints/PositiveIntegerValidator.java @@ -0,0 +1,36 @@ +package com.quorum.tessera.config.constraints; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class PositiveIntegerValidator implements ConstraintValidator { + + + private ValidPositiveInteger validPositiveInteger; + + @Override + public void initialize(ValidPositiveInteger constraintAnnotation) { + this.validPositiveInteger = constraintAnnotation; + } + + @Override + public boolean isValid(String secretVersion, ConstraintValidatorContext constraintValidatorContext) { + if(secretVersion == null) { + return true; + } + + Integer i; + + try { + i = Integer.valueOf(secretVersion); + } catch(NumberFormatException e) { + return false; + } + + if(i < 0) { + return false; + } + + return true; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java index 8f981b784c..05a7337282 100644 --- a/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java +++ b/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java @@ -5,6 +5,7 @@ import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.Objects; +import java.util.stream.Stream; public class UnsupportedKeyPairValidator implements ConstraintValidator { @@ -32,6 +33,11 @@ else if(isIncompleteAzureVaultKeyPair(keyPair)) { context.buildConstraintViolationWithTemplate("{UnsupportedKeyPair.bothAzureKeysRequired.message}") .addConstraintViolation(); } + else if(isIncompleteHashicorpVaultKeyPair(keyPair)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}") + .addConstraintViolation(); + } else if(isIncompleteFilesystemKeyPair(keyPair)) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("{UnsupportedKeyPair.bothFilesystemKeysRequired.message}") @@ -42,22 +48,38 @@ else if(isIncompleteFilesystemKeyPair(keyPair)) { } private boolean isIncompleteDirectKeyPair(UnsupportedKeyPair keyPair) { - return isOnlyOneInputNull(keyPair.getPublicKey(), keyPair.getPrivateKey()); + return isIncomplete(keyPair.getPublicKey(), keyPair.getPrivateKey()); } private boolean isIncompleteInlineKeyPair(UnsupportedKeyPair keyPair) { - return isOnlyOneInputNull(keyPair.getPublicKey(), keyPair.getConfig()); + return isIncomplete(keyPair.getPublicKey(), keyPair.getConfig()); } private boolean isIncompleteAzureVaultKeyPair(UnsupportedKeyPair keyPair) { - return isOnlyOneInputNull(keyPair.getAzureVaultPublicKeyId(), keyPair.getAzureVaultPrivateKeyId()); + return isIncomplete(keyPair.getAzureVaultPublicKeyId(), keyPair.getAzureVaultPrivateKeyId()); + } + + private boolean isIncompleteHashicorpVaultKeyPair(UnsupportedKeyPair keyPair) { + return isIncomplete(keyPair.getHashicorpVaultPublicKeyId(), keyPair.getHashicorpVaultPrivateKeyId(), keyPair.getHashicorpVaultSecretEngineName(), keyPair.getHashicorpVaultSecretName()); } private boolean isIncompleteFilesystemKeyPair(UnsupportedKeyPair keyPair) { - return isOnlyOneInputNull(keyPair.getPublicKeyPath(), keyPair.getPrivateKeyPath()); + return isIncomplete(keyPair.getPublicKeyPath(), keyPair.getPrivateKeyPath()); + } + + private boolean isIncomplete(Object ...args) { + if(areAnyNull(args) && areAnyNonNull(args)) { + return true; + } + return false; } - private boolean isOnlyOneInputNull(Object obj1, Object obj2) { - return Objects.isNull(obj1) ^ Objects.isNull(obj2); + private boolean areAnyNull(Object... args) { + return Stream.of(args).anyMatch(Objects::isNull); } + + private boolean areAnyNonNull(Object... args) { + return Stream.of(args).anyMatch(Objects::nonNull); + } + } diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/ValidPositiveInteger.java b/config/src/main/java/com/quorum/tessera/config/constraints/ValidPositiveInteger.java new file mode 100644 index 0000000000..9b6b436931 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/constraints/ValidPositiveInteger.java @@ -0,0 +1,29 @@ +package com.quorum.tessera.config.constraints; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE,TYPE_PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Constraint(validatedBy = PositiveIntegerValidator.class) +@Documented +public @interface ValidPositiveInteger { + + String message() default "{ValidPositiveInteger.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + boolean checkExists() default false; + + boolean checkCanCreate() default false; + + +} diff --git a/config/src/main/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPair.java b/config/src/main/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPair.java new file mode 100644 index 0000000000..18a90a4f68 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPair.java @@ -0,0 +1,88 @@ +package com.quorum.tessera.config.keypairs; + +import com.quorum.tessera.config.constraints.ValidPositiveInteger; + +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlElement; + +public class HashicorpVaultKeyPair implements ConfigKeyPair { + + @NotNull + @XmlElement + private String publicKeyId; + + @NotNull + @XmlElement + private String privateKeyId; + + @NotNull + @XmlElement + private String secretEngineName; + + @NotNull + @XmlElement + private String secretName; + + @ValidPositiveInteger + @XmlElement + private String secretVersion; + + public HashicorpVaultKeyPair(String publicKeyId, String privateKeyId, String secretEngineName, String secretName, String secretVersion) { + this.publicKeyId = publicKeyId; + this.privateKeyId = privateKeyId; + this.secretEngineName = secretEngineName; + this.secretName = secretName; + this.secretVersion = secretVersion; + } + + public String getPublicKeyId() { + return publicKeyId; + } + + public String getPrivateKeyId() { + return privateKeyId; + } + + public String getSecretEngineName() { + return secretEngineName; + } + + public String getSecretName() { + return secretName; + } + + public String getSecretVersion() { + return secretVersion; + } + + public Integer getSecretVersionAsInt() { + if(secretVersion == null) { + return 0; + } else { + return Integer.parseInt(secretVersion); + } + } + + @Override + public String getPublicKey() { + //keys are not fetched from vault yet so return null + return null; + } + + @Override + public String getPrivateKey() { + //keys are not fetched from vault yet so return null + return null; + } + + @Override + public void withPassword(String password) { + //password not used with vault stored keys + } + + @Override + public String getPassword() { + //no password to return + return ""; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPair.java b/config/src/main/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPair.java index dd84b4a168..3a1a015680 100644 --- a/config/src/main/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPair.java +++ b/config/src/main/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPair.java @@ -12,29 +12,44 @@ public class UnsupportedKeyPair implements ConfigKeyPair { @XmlElement - private final KeyDataConfig config; + private KeyDataConfig config; @XmlElement - private final String privateKey; + private String privateKey; @XmlElement - private final String publicKey; + private String publicKey; @XmlElement @XmlJavaTypeAdapter(PathAdapter.class) - private final Path privateKeyPath; + private Path privateKeyPath; @XmlElement @XmlJavaTypeAdapter(PathAdapter.class) - private final Path publicKeyPath; + private Path publicKeyPath; @XmlElement - private final String azureVaultPublicKeyId; + private String azureVaultPublicKeyId; @XmlElement - private final String azureVaultPrivateKeyId; + private String azureVaultPrivateKeyId; - public UnsupportedKeyPair(KeyDataConfig config, String privateKey, String publicKey, Path privateKeyPath, Path publicKeyPath, String azureVaultPublicKeyId, String azureVaultPrivateKeyId) { + @XmlElement + private String hashicorpVaultPublicKeyId; + + @XmlElement + private String hashicorpVaultPrivateKeyId; + + @XmlElement + private String hashicorpVaultSecretEngineName; + + @XmlElement + private String hashicorpVaultSecretName; + + @XmlElement + private String hashicorpVaultSecretVersion; + + public UnsupportedKeyPair(KeyDataConfig config, String privateKey, String publicKey, Path privateKeyPath, Path publicKeyPath, String azureVaultPublicKeyId, String azureVaultPrivateKeyId, String hashicorpVaultPublicKeyId, String hashicorpVaultPrivateKeyId, String hashicorpVaultSecretEngineName, String hashicorpVaultSecretName, String hashicorpVaultSecretVersion) { this.config = config; this.privateKey = privateKey; this.publicKey = publicKey; @@ -42,6 +57,15 @@ public UnsupportedKeyPair(KeyDataConfig config, String privateKey, String public this.publicKeyPath = publicKeyPath; this.azureVaultPublicKeyId = azureVaultPublicKeyId; this.azureVaultPrivateKeyId = azureVaultPrivateKeyId; + this.hashicorpVaultPublicKeyId = hashicorpVaultPublicKeyId; + this.hashicorpVaultPrivateKeyId = hashicorpVaultPrivateKeyId; + this.hashicorpVaultSecretEngineName = hashicorpVaultSecretEngineName; + this.hashicorpVaultSecretName = hashicorpVaultSecretName; + this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; + } + + public UnsupportedKeyPair() { + } @Override @@ -74,6 +98,26 @@ public String getAzureVaultPrivateKeyId() { return azureVaultPrivateKeyId; } + public String getHashicorpVaultPublicKeyId() { + return hashicorpVaultPublicKeyId; + } + + public String getHashicorpVaultPrivateKeyId() { + return hashicorpVaultPrivateKeyId; + } + + public String getHashicorpVaultSecretEngineName() { + return hashicorpVaultSecretEngineName; + } + + public String getHashicorpVaultSecretName() { + return hashicorpVaultSecretName; + } + + public String getHashicorpVaultSecretVersion() { + return hashicorpVaultSecretVersion; + } + @Override public void withPassword(String password) { //do nothing as password not used with this keypair type @@ -84,4 +128,51 @@ public String getPassword() { return null; } + public void setConfig(KeyDataConfig config) { + this.config = config; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public void setPrivateKeyPath(Path privateKeyPath) { + this.privateKeyPath = privateKeyPath; + } + + public void setPublicKeyPath(Path publicKeyPath) { + this.publicKeyPath = publicKeyPath; + } + + public void setAzureVaultPublicKeyId(String azureVaultPublicKeyId) { + this.azureVaultPublicKeyId = azureVaultPublicKeyId; + } + + public void setAzureVaultPrivateKeyId(String azureVaultPrivateKeyId) { + this.azureVaultPrivateKeyId = azureVaultPrivateKeyId; + } + + public void setHashicorpVaultPublicKeyId(String hashicorpVaultPublicKeyId) { + this.hashicorpVaultPublicKeyId = hashicorpVaultPublicKeyId; + } + + public void setHashicorpVaultPrivateKeyId(String hashicorpVaultPrivateKeyId) { + this.hashicorpVaultPrivateKeyId = hashicorpVaultPrivateKeyId; + } + + public void setHashicorpVaultSecretEngineName(String hashicorpVaultSecretEngineName) { + this.hashicorpVaultSecretEngineName = hashicorpVaultSecretEngineName; + } + + public void setHashicorpVaultSecretName(String hashicorpVaultSecretName) { + this.hashicorpVaultSecretName = hashicorpVaultSecretName; + } + + public void setHashicorpVaultSecretVersion(String hashicorpVaultSecretVersion) { + this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; + } } diff --git a/config/src/main/java/com/quorum/tessera/config/util/EnvironmentVariableProvider.java b/config/src/main/java/com/quorum/tessera/config/util/EnvironmentVariableProvider.java index bd90561327..cdd64aefab 100644 --- a/config/src/main/java/com/quorum/tessera/config/util/EnvironmentVariableProvider.java +++ b/config/src/main/java/com/quorum/tessera/config/util/EnvironmentVariableProvider.java @@ -1,5 +1,7 @@ package com.quorum.tessera.config.util; +import java.util.Optional; + //Provide a mockable wrapper for environment variable retrieval public class EnvironmentVariableProvider { @@ -7,4 +9,10 @@ public String getEnv(String name) { return System.getenv(name); } + public char[] getEnvAsCharArray(String name) { + return Optional.ofNullable(System.getenv(name)) + .map(String::toCharArray) + .orElse(null); + } + } diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/AzureGetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/AzureGetSecretData.java new file mode 100644 index 0000000000..4aec1d2d16 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/AzureGetSecretData.java @@ -0,0 +1,21 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +public class AzureGetSecretData implements GetSecretData { + + private String secretName; + + public AzureGetSecretData(String secretName) { + this.secretName = secretName; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.AZURE; + } + + public String getSecretName() { + return secretName; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/AzureSetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/AzureSetSecretData.java new file mode 100644 index 0000000000..d40a1f1d08 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/AzureSetSecretData.java @@ -0,0 +1,27 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +public class AzureSetSecretData implements SetSecretData { + private String secretName; + + private String secret; + + public AzureSetSecretData(String secretName, String secret) { + this.secretName = secretName; + this.secret = secret; + } + + public String getSecretName() { + return secretName; + } + + public String getSecret() { + return secret; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.AZURE; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/GetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/GetSecretData.java new file mode 100644 index 0000000000..2ec5dc1499 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/GetSecretData.java @@ -0,0 +1,7 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +public interface GetSecretData { + KeyVaultType getType(); +} diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretData.java new file mode 100644 index 0000000000..597d926991 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretData.java @@ -0,0 +1,38 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +public class HashicorpGetSecretData implements GetSecretData { + private final String secretEngineName; + private final String secretName; + private final String valueId; + private final int secretVersion; + + public HashicorpGetSecretData(String secretEngineName, String secretName, String valueId, int secretVersion) { + this.secretEngineName = secretEngineName; + this.secretName = secretName; + this.valueId = valueId; + this.secretVersion = secretVersion; + } + + public String getSecretEngineName() { + return secretEngineName; + } + + public String getSecretName() { + return secretName; + } + + public String getValueId() { + return valueId; + } + + public int getSecretVersion() { + return secretVersion; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.HASHICORP; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretData.java new file mode 100644 index 0000000000..eff9e0c261 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretData.java @@ -0,0 +1,34 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +import java.util.Map; + +public class HashicorpSetSecretData implements SetSecretData { + private final String secretEngineName; + private final String secretName; + private final Map nameValuePairs; + + public HashicorpSetSecretData(String secretEngineName, String secretName, Map nameValuePairs) { + this.secretEngineName = secretEngineName; + this.secretName = secretName; + this.nameValuePairs = nameValuePairs; + } + + public String getSecretEngineName() { + return secretEngineName; + } + + public String getSecretName() { + return secretName; + } + + public Map getNameValuePairs() { + return nameValuePairs; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.HASHICORP; + } +} diff --git a/config/src/main/java/com/quorum/tessera/config/vault/data/SetSecretData.java b/config/src/main/java/com/quorum/tessera/config/vault/data/SetSecretData.java new file mode 100644 index 0000000000..825cc0b918 --- /dev/null +++ b/config/src/main/java/com/quorum/tessera/config/vault/data/SetSecretData.java @@ -0,0 +1,7 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; + +public interface SetSecretData { + KeyVaultType getType(); +} diff --git a/config/src/main/resources/ValidationMessages.properties b/config/src/main/resources/ValidationMessages.properties index 0eb6a73f37..301defbee8 100644 --- a/config/src/main/resources/ValidationMessages.properties +++ b/config/src/main/resources/ValidationMessages.properties @@ -16,8 +16,12 @@ UnsupportedKeyPair.message=Invalid key-pair. Ensure that the public and private UnsupportedKeyPair.bothDirectKeysRequired.message=Invalid direct key-pair. Ensure that both the public and private keys for the key-pair have been provided. UnsupportedKeyPair.bothInlineKeysRequired.message=Invalid inline key-pair. Ensure that both the public key and private key config for the key-pair have been provided. UnsupportedKeyPair.bothAzureKeysRequired.message=Invalid Azure Key Vault key-pair. Ensure that both the public key ID and private key ID for the key-pair have been provided. +UnsupportedKeyPair.allHashicorpKeyDataRequired.message=Invalid Hashicorp Key Vault key-pair. Ensure that the public key ID, private key ID and secret path for the key-pair have been provided. UnsupportedKeyPair.bothFilesystemKeysRequired.message=Invalid filesystem key-pair. Ensure that both the public key path and private key path for the key-pair have been provided. ValidKeyDataConfig.message=A locked key was provided without a password.\n Please ensure the same number of passwords are provided as there are keys and remember to include empty passwords for unlocked keys InlineKeyData.message=A locked key was provided without a password.\n Please ensure the same number of passwords are provided as there are keys and remember to include empty passwords for unlocked keys ValidKeyConfiguration.message=A password file and inline passwords were provided. Please choose one or the other -ValidKeyVaultConfiguration.message=No azureKeyVaultConfig was specified but azureVaultPublicKeyId and azureVaultPrivateKeyId were provided +ValidKeyVaultConfiguration.message=No key vault configuration was specified but vault key data was provided +ValidKeyVaultConfiguration.azure.message=No azureKeyVaultConfig was specified but azureVaultPublicKeyId and azureVaultPrivateKeyId were provided +ValidKeyVaultConfiguration.hashicorp.message=No hashicorpKeyVaultConfig was specified but Hashicorp keyData was provided +ValidPositiveInteger.message=The value provided must be an integer equal to 0 or greater \ No newline at end of file diff --git a/config/src/test/java/com/quorum/tessera/config/HashicorpKeyVaultConfigTest.java b/config/src/test/java/com/quorum/tessera/config/HashicorpKeyVaultConfigTest.java new file mode 100644 index 0000000000..12797d874f --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/HashicorpKeyVaultConfigTest.java @@ -0,0 +1,70 @@ +package com.quorum.tessera.config; + +import org.junit.Before; +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpKeyVaultConfigTest { + + private HashicorpKeyVaultConfig vaultConfig; + + @Before + public void setUp() { + vaultConfig = new HashicorpKeyVaultConfig(); + } + + @Test + public void multiArgConstructor() { + String url = "url"; + String approle = "approle"; + Path keyStore = Paths.get("keystore"); + Path trustStore = Paths.get("truststore"); + + HashicorpKeyVaultConfig conf = new HashicorpKeyVaultConfig(url, approle, keyStore, trustStore); + + assertThat(conf.getUrl()).isEqualTo(url); + assertThat(conf.getApprolePath()).isEqualTo("approle"); + assertThat(conf.getTlsKeyStorePath()).isEqualTo(keyStore); + assertThat(conf.getTlsTrustStorePath()).isEqualTo(trustStore); + } + + @Test + public void gettersAndSetters() { + assertThat(vaultConfig.getUrl()).isEqualTo(null); + assertThat(vaultConfig.getTlsKeyStorePath()).isEqualTo(null); + assertThat(vaultConfig.getTlsTrustStorePath()).isEqualTo(null); + + String url = "url"; + Path keyStore = Paths.get("keystore"); + Path trustStore = Paths.get("truststore"); + + vaultConfig.setUrl(url); + vaultConfig.setTlsKeyStorePath(keyStore); + vaultConfig.setTlsTrustStorePath(trustStore); + + assertThat(vaultConfig.getUrl()).isEqualTo(url); + assertThat(vaultConfig.getTlsKeyStorePath()).isEqualTo(keyStore); + assertThat(vaultConfig.getTlsTrustStorePath()).isEqualTo(trustStore); + } + + @Test + public void getType() { + assertThat(vaultConfig.getKeyVaultType()).isEqualTo(KeyVaultType.HASHICORP); + } + + @Test + public void getApprolePathReturnsDefaultIfNotSet() { + assertThat(vaultConfig.getApprolePath()).isEqualTo("approle"); + } + + @Test + public void getApprolePath() { + vaultConfig.setApprolePath("notdefault"); + assertThat(vaultConfig.getApprolePath()).isEqualTo("notdefault"); + } + +} diff --git a/config/src/test/java/com/quorum/tessera/config/ValidationTest.java b/config/src/test/java/com/quorum/tessera/config/ValidationTest.java index 936d88238b..961a7b0d66 100644 --- a/config/src/test/java/com/quorum/tessera/config/ValidationTest.java +++ b/config/src/test/java/com/quorum/tessera/config/ValidationTest.java @@ -1,10 +1,6 @@ package com.quorum.tessera.config; -import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; -import com.quorum.tessera.config.keypairs.ConfigKeyPair; -import com.quorum.tessera.config.keypairs.DirectKeyPair; -import com.quorum.tessera.config.keypairs.FilesystemKeyPair; -import com.quorum.tessera.config.keypairs.InlineKeypair; +import com.quorum.tessera.config.keypairs.*; import org.junit.Test; import org.mockito.Mockito; @@ -17,9 +13,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class ValidationTest { @@ -60,7 +54,12 @@ public void validateArgonOptionsAllNullAlgoHasDefaultValue() { public void keyDataConfigMissingPassword() { PrivateKeyData privateKeyData = new PrivateKeyData(null, "snonce", "asalt", "sbox", mock(ArgonOptions.class), null); KeyDataConfig keyDataConfig = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); - KeyData keyData = new KeyData(keyDataConfig, "privateKey", "publicKey", null, null, null, null); + + KeyData keyData = new KeyData(); + keyData.setConfig(keyDataConfig); + keyData.setPublicKey("publicKey"); + keyData.setPrivateKey("privateKey"); + Set> violations = validator.validate(keyData); assertThat(violations).hasSize(1); @@ -74,7 +73,12 @@ public void keyDataConfigMissingPassword() { public void keyDataConfigNaclFailure() { PrivateKeyData privateKeyData = new PrivateKeyData(null, "snonce", "asalt", "sbox", mock(ArgonOptions.class), "SECRET"); KeyDataConfig keyDataConfig = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); - KeyData keyData = new KeyData(keyDataConfig, "NACL_FAILURE", "publicKey", null, null, null, null); + + KeyData keyData = new KeyData(); + keyData.setConfig(keyDataConfig); + keyData.setPrivateKey("NACL_FAILURE"); + keyData.setPublicKey("publicKey"); + Set> violations = validator.validate(keyData); assertThat(violations).hasSize(1); @@ -88,7 +92,12 @@ public void keyDataConfigNaclFailure() { public void keyDataConfigInvalidBase64() { PrivateKeyData privateKeyData = new PrivateKeyData(null, "snonce", "asalt", "sbox", mock(ArgonOptions.class), "SECRET"); KeyDataConfig keyDataConfig = new KeyDataConfig(privateKeyData, PrivateKeyType.LOCKED); - KeyData keyData = new KeyData(keyDataConfig, "INAVLID_BASE", "publicKey", null, null, null, null); + + KeyData keyData = new KeyData(); + keyData.setConfig(keyDataConfig); + keyData.setPrivateKey("INVALID_BASE"); + keyData.setPublicKey("publicKey"); + Set> violations = validator.validate(keyData); assertThat(violations).hasSize(1); @@ -107,7 +116,7 @@ public void inlineKeyPairNoPasswordProvided() { InlineKeypair spy = Mockito.spy(new InlineKeypair("validkey", keyConfig)); doReturn("validkey").when(spy).getPrivateKey(); - KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(spy), null); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(spy), null, null); Set> violations = validator.validate(keyConfiguration); @@ -208,7 +217,7 @@ public void keypairPathsValidation() { final ConfigKeyPair keyPair = new FilesystemKeyPair(publicKeyPath, privateKeyPath); - final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null); + final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null, null); final Set> violations = validator.validate(keyConfiguration); assertThat(violations).hasSize(2); @@ -232,7 +241,7 @@ public void keypairInlineValidation() { final ConfigKeyPair keyPair = new DirectKeyPair("notvalidbase64", "c=="); - KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null, null); Set> violations = validator.validate(keyConfiguration); assertThat(violations).hasSize(1); @@ -268,28 +277,86 @@ public void azureKeyPairIdsDisallowedCharactersCreateViolation() { @Test public void azureKeyPairProvidedWithoutKeyVaultConfigCreatesViolation() { AzureVaultKeyPair keyPair = new AzureVaultKeyPair("publicVauldId", "privateVaultId"); - KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null, null); Config config = new Config(null, null, null, keyConfiguration, null, null, false, false); Set> violations = validator.validateProperty(config, "keys"); assertThat(violations).hasSize(1); ConstraintViolation violation = violations.iterator().next(); - assertThat(violation.getMessageTemplate()).isEqualTo("{ValidKeyVaultConfiguration.message}"); + assertThat(violation.getMessageTemplate()).isEqualTo("{ValidKeyVaultConfiguration.azure.message}"); + } + + @Test + public void azureKeyPairProvidedWithHashicorpKeyVaultConfigCreatesViolation() { + AzureVaultKeyPair keyPair = new AzureVaultKeyPair("publicVauldId", "privateVaultId"); + + KeyConfiguration keyConfiguration = new KeyConfiguration(); + keyConfiguration.setKeyData(singletonList(keyPair)); + + HashicorpKeyVaultConfig hashicorpConfig = new HashicorpKeyVaultConfig(); + keyConfiguration.setHashicorpKeyVaultConfig(hashicorpConfig); + + Config config = new Config(); + config.setKeys(keyConfiguration); + + Set> violations = validator.validateProperty(config, "keys"); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getMessageTemplate()).isEqualTo("{ValidKeyVaultConfiguration.azure.message}"); + } + + @Test + public void hashicorpKeyPairProvidedWithoutKeyVaultConfigCreatesViolation() { + HashicorpVaultKeyPair keyPair = new HashicorpVaultKeyPair("pubId", "privdId", "secretEngine", "secretName", null); + + KeyConfiguration keyConfiguration = new KeyConfiguration(); + keyConfiguration.setKeyData(singletonList(keyPair)); + keyConfiguration.setHashicorpKeyVaultConfig(null); + + Config config = new Config(); + config.setKeys(keyConfiguration); + + Set> violations = validator.validateProperty(config, "keys"); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getMessageTemplate()).isEqualTo("{ValidKeyVaultConfiguration.hashicorp.message}"); } @Test - public void nonKeyVaultPairProvidedWithoutKeyVaultConfigDoesNotCreateViolation() { + public void hashicorpKeyPairProvidedWithAzureKeyVaultConfigCreatesViolation() { + HashicorpVaultKeyPair keyPair = new HashicorpVaultKeyPair("pubId", "privdId", "secretEngine", "secretName", null); + + KeyConfiguration keyConfiguration = new KeyConfiguration(); + keyConfiguration.setKeyData(singletonList(keyPair)); + keyConfiguration.setHashicorpKeyVaultConfig(null); + + AzureKeyVaultConfig azureConfig = new AzureKeyVaultConfig(); + keyConfiguration.setAzureKeyVaultConfig(azureConfig); + + Config config = new Config(); + config.setKeys(keyConfiguration); + + Set> violations = validator.validateProperty(config, "keys"); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getMessageTemplate()).isEqualTo("{ValidKeyVaultConfiguration.hashicorp.message}"); + } + + @Test + public void nonKeyVaultPairProvidedWithoutAzureAndHashicorpKeyVaultConfigDoesNotCreateViolation() { DirectKeyPair keyPair = new DirectKeyPair("pub", "priv"); - KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null, null); Config config = new Config(null, null, null, keyConfiguration, null, null, false, false); Set> violations = validator.validateProperty(config, "keys"); assertThat(violations).hasSize(0); } - @Test public void keyConfigurationIsNullCreatesNotNullViolation() { Config config = new Config(null, null, null, null, null, null, false, false); @@ -314,10 +381,10 @@ public void azureVaultConfigWithNoUrlCreatesNullViolation() { } @Test - public void azureVaultKeyPairProvidedButKeyVaultConfigHasNullUrlCreatesNullViolation() { + public void azureVaultKeyPairProvidedButKeyVaultConfigHasNullUrlCreatesNotNullViolation() { AzureVaultKeyPair keyPair = new AzureVaultKeyPair("pubId", "privId"); AzureKeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig(null); - KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), keyVaultConfig); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), keyVaultConfig, null); Set> violations = validator.validate(keyConfiguration); assertThat(violations).hasSize(1); @@ -326,4 +393,29 @@ public void azureVaultKeyPairProvidedButKeyVaultConfigHasNullUrlCreatesNullViola assertThat(violation.getMessageTemplate()).isEqualTo("{javax.validation.constraints.NotNull.message}"); assertThat(violation.getPropertyPath().toString()).isEqualTo("azureKeyVaultConfig.url"); } + + @Test + public void hashicorpVaultConfigWithNoUrlCreatesNotNullViolation() { + HashicorpKeyVaultConfig keyVaultConfig = new HashicorpKeyVaultConfig(); + + Set> violations = validator.validate(keyVaultConfig); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getMessageTemplate()).isEqualTo("{javax.validation.constraints.NotNull.message}"); + } + + @Test + public void hashicorpVaultKeyPairProvidedButKeyVaultConfigHasNullUrlCreatesNotNullViolation() { + HashicorpVaultKeyPair keyPair = new HashicorpVaultKeyPair("pubId", "privId", "secretEngine", "secretName", null); + HashicorpKeyVaultConfig keyVaultConfig = new HashicorpKeyVaultConfig(); + KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keyPair), null, keyVaultConfig); + + Set> violations = validator.validate(keyConfiguration); + assertThat(violations).hasSize(1); + + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getMessageTemplate()).isEqualTo("{javax.validation.constraints.NotNull.message}"); + assertThat(violation.getPropertyPath().toString()).isEqualTo("hashicorpKeyVaultConfig.url"); + } } diff --git a/config/src/test/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapterTest.java b/config/src/test/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapterTest.java index 53b7a4ae55..c978f65e70 100644 --- a/config/src/test/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapterTest.java +++ b/config/src/test/java/com/quorum/tessera/config/adapters/KeyConfigurationAdapterTest.java @@ -20,7 +20,7 @@ public class KeyConfigurationAdapterTest { @Test public void marshallingDoesNothing() { - final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, emptyList(), null); + final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, emptyList(), null, null); final KeyConfiguration marshalled = this.keyConfigurationAdapter.marshal(keyConfiguration); @@ -33,7 +33,7 @@ public void emptyPasswordsReturnsSameKeys() { //null paths since we won't actually be reading them final ConfigKeyPair keypair = new FilesystemKeyPair(null, null); - final KeyConfiguration keyConfiguration = new KeyConfiguration(null, emptyList(), singletonList(keypair), null); + final KeyConfiguration keyConfiguration = new KeyConfiguration(null, emptyList(), singletonList(keypair), null, null); final KeyConfiguration configuration = this.keyConfigurationAdapter.unmarshal(keyConfiguration); @@ -50,7 +50,7 @@ public void noPasswordsReturnsSameKeys() { //null paths since we won't actually be reading them final ConfigKeyPair keypair = new FilesystemKeyPair(null, null); - final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keypair), null); + final KeyConfiguration keyConfiguration = new KeyConfiguration(null, null, singletonList(keypair), null, null); final KeyConfiguration configuration = this.keyConfigurationAdapter.unmarshal(keyConfiguration); @@ -68,7 +68,7 @@ public void passwordsAssignedToKeys() { //null paths since we won't actually be reading them final ConfigKeyPair keypair = new FilesystemKeyPair(null, null); final KeyConfiguration keyConfiguration - = new KeyConfiguration(null, singletonList("passwordsAssignedToKeys"), singletonList(keypair), null); + = new KeyConfiguration(null, singletonList("passwordsAssignedToKeys"), singletonList(keypair), null, null); final KeyConfiguration configuration = this.keyConfigurationAdapter.unmarshal(keyConfiguration); @@ -83,7 +83,7 @@ public void unreadablePasswordFileGivesNoPasswords() throws IOException { final Path passes = Files.createTempDirectory("testdirectory").resolve("nonexistantfile.txt"); final ConfigKeyPair keypair = new FilesystemKeyPair(null, null); - final KeyConfiguration keyConfiguration = new KeyConfiguration(passes, null, singletonList(keypair), null); + final KeyConfiguration keyConfiguration = new KeyConfiguration(passes, null, singletonList(keypair), null, null); final KeyConfiguration configuration = this.keyConfigurationAdapter.unmarshal(keyConfiguration); diff --git a/config/src/test/java/com/quorum/tessera/config/adapters/KeyDataAdapterTest.java b/config/src/test/java/com/quorum/tessera/config/adapters/KeyDataAdapterTest.java index 4407c3d492..b51c3bd739 100644 --- a/config/src/test/java/com/quorum/tessera/config/adapters/KeyDataAdapterTest.java +++ b/config/src/test/java/com/quorum/tessera/config/adapters/KeyDataAdapterTest.java @@ -21,7 +21,9 @@ public class KeyDataAdapterTest { @Test public void marshallDirectKeys() { final ConfigKeyPair keys = new DirectKeyPair("PUB", "PRIV"); - final KeyData expected = new KeyData(null, "PRIV", "PUB", null, null, null, null); + final KeyData expected = new KeyData(); + expected.setPublicKey("PUB"); + expected.setPrivateKey("PRIV"); final KeyData marshalledKey = adapter.marshal(keys); @@ -32,7 +34,10 @@ public void marshallDirectKeys() { public void marshallInlineKeys() { final PrivateKeyData pkd = new PrivateKeyData("val", null, null, null, null, null); final ConfigKeyPair keys = new InlineKeypair("PUB", new KeyDataConfig(pkd, UNLOCKED)); - final KeyData expected = new KeyData(new KeyDataConfig(pkd, UNLOCKED), null, "PUB", null, null, null, null); + final KeyData expected = new KeyData(); + + expected.setPublicKey("PUB"); + expected.setConfig(new KeyDataConfig(pkd, UNLOCKED)); final KeyData marshalledKey = adapter.marshal(keys); @@ -44,7 +49,10 @@ public void marshallFilesystemKeys() { final Path path = mock(Path.class); final FilesystemKeyPair keyPair = new FilesystemKeyPair(path, path); - final KeyData expected = new KeyData(null, null, null, path, path, null, null); + final KeyData expected = new KeyData(); + expected.setPublicKeyPath(path); + expected.setPrivateKeyPath(path); + final KeyData result = adapter.marshal(keyPair); assertThat(result).isEqualTo(expected); @@ -54,7 +62,25 @@ public void marshallFilesystemKeys() { public void marshallAzureKeys() { final AzureVaultKeyPair keyPair = new AzureVaultKeyPair("pubId", "privId"); - final KeyData expected = new KeyData(null, null, null, null, null, "privId", "pubId"); + final KeyData expected = new KeyData(); + expected.setAzureVaultPublicKeyId("pubId"); + expected.setAzureVaultPrivateKeyId("privId"); + + final KeyData result = adapter.marshal(keyPair); + + assertThat(result).isEqualTo(expected); + } + + @Test + public void marshallHashicorpKeys() { + final HashicorpVaultKeyPair keyPair = new HashicorpVaultKeyPair("pubId", "privId", "secretEngineName", "secretName", "0"); + + final KeyData expected = new KeyData(); + expected.setHashicorpVaultPublicKeyId("pubId"); + expected.setHashicorpVaultPrivateKeyId("privId"); + expected.setHashicorpVaultSecretEngineName("secretEngineName"); + expected.setHashicorpVaultSecretName("secretName"); + final KeyData result = adapter.marshal(keyPair); assertThat(result).isEqualTo(expected); @@ -64,9 +90,14 @@ public void marshallAzureKeys() { public void marshallUnsupportedKeys() { final KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); final Path path = mock(Path.class); - final UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, "priv", null, path, null, null, null); + final UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, "priv", null, path, null, null, null, null, null, null, null, null); + + final KeyData expected = new KeyData(); + //set a random selection of values that are not sufficient to make a complete key pair of any type + expected.setConfig(keyDataConfig); + expected.setPrivateKey("priv"); + expected.setPrivateKeyPath(path); - final KeyData expected = new KeyData(keyDataConfig, "priv", null, path, null, null, null); final KeyData result = adapter.marshal(keyPair); assertThat(result).isEqualTo(expected); @@ -118,7 +149,9 @@ public void marshallLockedKeyNullifiesPrivateKey() { @Test public void unmarshallingDirectKeysGivesCorrectKeypair() { - final KeyData input = new KeyData(null, "private", "public", null, null, null, null); + final KeyData input = new KeyData(); + input.setPublicKey("public"); + input.setPrivateKey("private"); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(DirectKeyPair.class); @@ -127,7 +160,9 @@ public void unmarshallingDirectKeysGivesCorrectKeypair() { @Test public void unmarshallingInlineKeysGivesCorrectKeypair() { - final KeyData input = new KeyData(new KeyDataConfig(null, null), null, "public", null, null, null, null); + final KeyData input = new KeyData(); + input.setPublicKey("public"); + input.setConfig(new KeyDataConfig(null, null)); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(InlineKeypair.class); @@ -136,7 +171,9 @@ public void unmarshallingInlineKeysGivesCorrectKeypair() { @Test public void unmarshallingFilesystemKeysGivesCorrectKeypair() { - final KeyData input = new KeyData(null, null, null, Paths.get("private"), Paths.get("public"), null, null); + final KeyData input = new KeyData(); + input.setPublicKeyPath(Paths.get("public")); + input.setPrivateKeyPath(Paths.get("private")); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(FilesystemKeyPair.class); @@ -144,15 +181,31 @@ public void unmarshallingFilesystemKeysGivesCorrectKeypair() { @Test public void unmarshallingAzureKeysGivesCorrectKeyPair() { - final KeyData input = new KeyData(null, null, null, null, null, "privId", "pubId"); + final KeyData input = new KeyData(); + input.setAzureVaultPublicKeyId("pubId"); + input.setAzureVaultPrivateKeyId("privId"); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(AzureVaultKeyPair.class); } + @Test + public void unmarshallingHashicorpKeysGivesCorrectKeyPair() { + final KeyData input = new KeyData(); + + input.setHashicorpVaultPublicKeyId("pubId"); + input.setHashicorpVaultPrivateKeyId("privId"); + input.setHashicorpVaultSecretEngineName("secretEngine"); + input.setHashicorpVaultSecretName("secretName"); + + final ConfigKeyPair result = this.adapter.unmarshal(input); + assertThat(result).isInstanceOf(HashicorpVaultKeyPair.class); + } + @Test public void unmarshallingPrivateOnlyGivesUnsupportedKeyPair() { - final KeyData input = new KeyData(null, "private", null, null, null, null, null); + final KeyData input = new KeyData(); + input.setPrivateKey("private"); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); @@ -161,7 +214,8 @@ public void unmarshallingPrivateOnlyGivesUnsupportedKeyPair() { @Test public void unmarshallingPrivateConfigOnlyGivesUnsupportedKeyPair() { final KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); - final KeyData input = new KeyData(keyDataConfig, null, null, null, null, null, null); + final KeyData input = new KeyData(); + input.setConfig(keyDataConfig); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); @@ -169,7 +223,8 @@ public void unmarshallingPrivateConfigOnlyGivesUnsupportedKeyPair() { @Test public void unmarshallingAzurePublicOnlyGivesUnsupportedKeyPair() { - final KeyData input = new KeyData(null, null, null, null, null, null, "pubId"); + final KeyData input = new KeyData(); + input.setAzureVaultPublicKeyId("pubId"); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); @@ -177,7 +232,44 @@ public void unmarshallingAzurePublicOnlyGivesUnsupportedKeyPair() { @Test public void unmarshallingAzurePrivateOnlyGivesUnsupportedKeyPair() { - final KeyData input = new KeyData(null, null, null, null, null, "priv", null); + final KeyData input = new KeyData(); + input.setAzureVaultPrivateKeyId("privId"); + + final ConfigKeyPair result = this.adapter.unmarshal(input); + assertThat(result).isInstanceOf(UnsupportedKeyPair.class); + } + + @Test + public void unmarshallingHashicorpPublicOnlyGivesUnsupprtedKeyPair() { + final KeyData input = new KeyData(); + input.setHashicorpVaultPublicKeyId("pubId"); + + final ConfigKeyPair result = this.adapter.unmarshal(input); + assertThat(result).isInstanceOf(UnsupportedKeyPair.class); + } + + @Test + public void unmarshallingHashicorpPrivateOnlyGivesUnsupprtedKeyPair() { + final KeyData input = new KeyData(); + input.setHashicorpVaultPrivateKeyId("privId"); + + final ConfigKeyPair result = this.adapter.unmarshal(input); + assertThat(result).isInstanceOf(UnsupportedKeyPair.class); + } + + @Test + public void unmarshallingHashicorpSecretEngineNameOnlyGivesUnsupprtedKeyPair() { + final KeyData input = new KeyData(); + input.setHashicorpVaultSecretEngineName("secretEngine"); + + final ConfigKeyPair result = this.adapter.unmarshal(input); + assertThat(result).isInstanceOf(UnsupportedKeyPair.class); + } + + @Test + public void unmarshallingHashicorpSecretNameOnlyGivesUnsupprtedKeyPair() { + final KeyData input = new KeyData(); + input.setHashicorpVaultSecretName("secretName"); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); @@ -186,7 +278,8 @@ public void unmarshallingAzurePrivateOnlyGivesUnsupportedKeyPair() { @Test public void unmarshallingPublicPathOnlyGivesUnsupportedKeyPair() { final Path path = mock(Path.class); - final KeyData input = new KeyData(null, null, null, null, path, null, null); + final KeyData input = new KeyData(); + input.setPublicKeyPath(path); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); @@ -195,7 +288,8 @@ public void unmarshallingPublicPathOnlyGivesUnsupportedKeyPair() { @Test public void unmarshallingPrivatePathOnlyGivesUnsupportedKeyPair() { final Path path = mock(Path.class); - final KeyData input = new KeyData(null, null, null, path, null, null, null); + final KeyData input = new KeyData(); + input.setPrivateKeyPath(path); final ConfigKeyPair result = this.adapter.unmarshal(input); assertThat(result).isInstanceOf(UnsupportedKeyPair.class); diff --git a/config/src/test/java/com/quorum/tessera/config/constraints/KeyConfigurationValidatorTest.java b/config/src/test/java/com/quorum/tessera/config/constraints/KeyConfigurationValidatorTest.java index 893e19de16..bc0919da80 100644 --- a/config/src/test/java/com/quorum/tessera/config/constraints/KeyConfigurationValidatorTest.java +++ b/config/src/test/java/com/quorum/tessera/config/constraints/KeyConfigurationValidatorTest.java @@ -23,7 +23,7 @@ public void init() { @Test public void bothNotSetIsValid() { - final KeyConfiguration configuration = new KeyConfiguration(null, null, null, null); + final KeyConfiguration configuration = new KeyConfiguration(null, null, null, null, null); assertThat(validator.isValid(configuration, mock(ConstraintValidatorContext.class))).isTrue(); @@ -32,7 +32,7 @@ public void bothNotSetIsValid() { @Test public void fileSetIsValid() { - final KeyConfiguration configuration = new KeyConfiguration(Paths.get("anything"), null, null, null); + final KeyConfiguration configuration = new KeyConfiguration(Paths.get("anything"), null, null, null, null); assertThat(validator.isValid(configuration, mock(ConstraintValidatorContext.class))).isTrue(); @@ -41,7 +41,7 @@ public void fileSetIsValid() { @Test public void inlineSetIsValid() { - final KeyConfiguration configuration = new KeyConfiguration(null, emptyList(), null, null); + final KeyConfiguration configuration = new KeyConfiguration(null, emptyList(), null, null, null); assertThat(validator.isValid(configuration, mock(ConstraintValidatorContext.class))).isTrue(); @@ -50,7 +50,7 @@ public void inlineSetIsValid() { @Test public void bothSetIsInvalid() { - final KeyConfiguration configuration = new KeyConfiguration(Paths.get("anything"), emptyList(), null, null); + final KeyConfiguration configuration = new KeyConfiguration(Paths.get("anything"), emptyList(), null, null, null); assertThat(validator.isValid(configuration, mock(ConstraintValidatorContext.class))).isFalse(); diff --git a/config/src/test/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidatorTest.java b/config/src/test/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidatorTest.java index 4b8622df80..32a4987616 100644 --- a/config/src/test/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidatorTest.java +++ b/config/src/test/java/com/quorum/tessera/config/constraints/KeyVaultConfigurationValidatorTest.java @@ -1,6 +1,7 @@ package com.quorum.tessera.config.constraints; import com.quorum.tessera.config.AzureKeyVaultConfig; +import com.quorum.tessera.config.HashicorpKeyVaultConfig; import com.quorum.tessera.config.KeyConfiguration; import com.quorum.tessera.config.keypairs.*; import org.junit.Before; @@ -14,7 +15,9 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class KeyVaultConfigurationValidatorTest { @@ -28,6 +31,9 @@ public class KeyVaultConfigurationValidatorTest { public void setUp() { context = mock(ConstraintValidatorContext.class); + ConstraintValidatorContext.ConstraintViolationBuilder builder = mock(ConstraintValidatorContext.ConstraintViolationBuilder.class); + when(context.buildConstraintViolationWithTemplate(any(String.class))).thenReturn(builder); + validator = new KeyVaultConfigurationValidator(); ValidKeyVaultConfiguration validKeyVaultConfiguration = mock(ValidKeyVaultConfiguration.class); validator.initialize(validKeyVaultConfiguration); @@ -39,7 +45,7 @@ public void nullKeyConfigurationIsAllowedAndWillBePickedUpByNotNullAnnotation() } @Test - public void keyVaultConfigWithVaultKeyPairIsValid() { + public void azureConfigWithAzureKeyPairIsValid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); AzureVaultKeyPair keyPair = mock(AzureVaultKeyPair.class); AzureKeyVaultConfig keyVaultConfig = mock(AzureKeyVaultConfig.class); @@ -51,7 +57,7 @@ public void keyVaultConfigWithVaultKeyPairIsValid() { } @Test - public void keyVaultConfigWithMultipleVaultKeyPairTypesIsValid() { + public void azureConfigWithMultipleAzureKeyPairsIsValid() { List keyPairs = new ArrayList<>(); keyPairs.add(mock(AzureVaultKeyPair.class)); keyPairs.add(mock(AzureVaultKeyPair.class)); @@ -66,7 +72,7 @@ public void keyVaultConfigWithMultipleVaultKeyPairTypesIsValid() { } @Test - public void keyVaultConfigWithMultipleKeyPairTypesIncludingVaultIsValid() { + public void azureConfigWithMultipleKeyPairTypesIncludingAzureIsValid() { List keyPairs = new ArrayList<>(); keyPairs.add(mock(AzureVaultKeyPair.class)); keyPairs.add(mock(DirectKeyPair.class)); @@ -81,7 +87,7 @@ public void keyVaultConfigWithMultipleKeyPairTypesIncludingVaultIsValid() { } @Test - public void keyVaultConfigWithNonVaultKeyPairIsValid() { + public void azureConfigWithNonAzureKeyPairIsValid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); DirectKeyPair keyPair = mock(DirectKeyPair.class); AzureKeyVaultConfig keyVaultConfig = mock(AzureKeyVaultConfig.class); @@ -93,7 +99,7 @@ public void keyVaultConfigWithNonVaultKeyPairIsValid() { } @Test - public void keyVaultConfigWithMultipleNonVaultKeyPairsIsValid() { + public void azureConfigWithMultipleNonAzureKeyPairsIsValid() { List keyPairs = new ArrayList<>(); keyPairs.add(mock(DirectKeyPair.class)); keyPairs.add(mock(InlineKeypair.class)); @@ -108,7 +114,7 @@ public void keyVaultConfigWithMultipleNonVaultKeyPairsIsValid() { } @Test - public void noKeyVaultConfigWithVaultKeyPairIsInvalid() { + public void noAzureConfigWithAzureKeyPairIsInvalid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); AzureVaultKeyPair keyPair = mock(AzureVaultKeyPair.class); @@ -116,10 +122,11 @@ public void noKeyVaultConfigWithVaultKeyPairIsInvalid() { when(keyConfiguration.getAzureKeyVaultConfig()).thenReturn(null); assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.azure.message}"); } @Test - public void noKeyVaultConfigWithMultipleVaultKeyPairsIsInvalid() { + public void noAzureConfigWithMultipleAzureKeyPairsIsInvalid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); List keyPairs = new ArrayList<>(); @@ -130,10 +137,11 @@ public void noKeyVaultConfigWithMultipleVaultKeyPairsIsInvalid() { when(keyConfiguration.getAzureKeyVaultConfig()).thenReturn(null); assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.azure.message}"); } @Test - public void noKeyVaultConfigWithMultipleKeyPairsIncludingVaultIsInvalid() { + public void noAzureConfigWithMultipleKeyPairsIncludingAzureIsInvalid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); List keyPairs = new ArrayList<>(); @@ -144,10 +152,11 @@ public void noKeyVaultConfigWithMultipleKeyPairsIncludingVaultIsInvalid() { when(keyConfiguration.getAzureKeyVaultConfig()).thenReturn(null); assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.azure.message}"); } @Test - public void noKeyVaultConfigWithNonVaultKeyPairIsValid() { + public void noAzureConfigWithNonAzureKeyPairIsValid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); DirectKeyPair keyPair = mock(DirectKeyPair.class); @@ -158,7 +167,7 @@ public void noKeyVaultConfigWithNonVaultKeyPairIsValid() { } @Test - public void noKeyVaultConfigWithMultipleNonVaultKeyPairsIsValid() { + public void noAzureConfigWithMultipleNonAzureKeyPairsIsValid() { KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); List keyPairs = new ArrayList<>(); @@ -171,4 +180,170 @@ public void noKeyVaultConfigWithMultipleNonVaultKeyPairsIsValid() { assertThat(validator.isValid(keyConfiguration, context)).isTrue(); } + @Test + public void hashicorpConfigWithHashicorpKeyPairIsValid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + HashicorpVaultKeyPair keyPair = mock(HashicorpVaultKeyPair.class); + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + + when(keyConfiguration.getKeyData()).thenReturn(Collections.singletonList(keyPair)); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void hashicorpConfigWithMultipleHashicorpKeyPairsIsValid() { + List keyPairs = new ArrayList<>(); + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void hashicorpConfigWithMultipleKeyPairTypesIncludingHashicorpIsValid() { + List keyPairs = new ArrayList<>(); + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + keyPairs.add(mock(DirectKeyPair.class)); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void hashicorpConfigWithNonHashicorpKeyPairIsValid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DirectKeyPair keyPair = mock(DirectKeyPair.class); + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + + when(keyConfiguration.getKeyData()).thenReturn(Collections.singletonList(keyPair)); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void hashicorpConfigWithMultipleNonHashicorpKeyPairsIsValid() { + List keyPairs = new ArrayList<>(); + keyPairs.add(mock(DirectKeyPair.class)); + keyPairs.add(mock(InlineKeypair.class)); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void noHashicorpConfigWithHashicorpKeyPairIsInvalid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + HashicorpVaultKeyPair keyPair = mock(HashicorpVaultKeyPair.class); + + when(keyConfiguration.getKeyData()).thenReturn(Collections.singletonList(keyPair)); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.hashicorp.message}"); + } + + @Test + public void noHashicorpConfigWithMultipleHashicorpKeyPairsIsInvalid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + List keyPairs = new ArrayList<>(); + + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.hashicorp.message}"); + } + + @Test + public void noHashicorpConfigWithMultipleKeyPairsIncludingHashicorpIsInvalid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + List keyPairs = new ArrayList<>(); + + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + keyPairs.add(mock(DirectKeyPair.class)); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.hashicorp.message}"); + } + + @Test + public void noHashicorpConfigWithNonHashicorpKeyPairIsValid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DirectKeyPair keyPair = mock(DirectKeyPair.class); + + when(keyConfiguration.getKeyData()).thenReturn(Collections.singletonList(keyPair)); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void noHashicorpConfigWithMultipleNonHashicorpKeyPairsIsValid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + List keyPairs = new ArrayList<>(); + + keyPairs.add(mock(DirectKeyPair.class)); + keyPairs.add(mock(FilesystemKeyPair.class)); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + assertThat(validator.isValid(keyConfiguration, context)).isTrue(); + } + + @Test + public void azureConfigWithHashicorpKeyPairIsInvalid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + List keyPairs = new ArrayList<>(); + + keyPairs.add(mock(HashicorpVaultKeyPair.class)); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + AzureKeyVaultConfig azureConfig = mock(AzureKeyVaultConfig.class); + when(keyConfiguration.getAzureKeyVaultConfig()).thenReturn(azureConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.hashicorp.message}"); + } + + @Test + public void hashicorpConfigWithAzureKeyPairIsInvalid() { + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + List keyPairs = new ArrayList<>(); + + keyPairs.add(mock(AzureVaultKeyPair.class)); + + when(keyConfiguration.getKeyData()).thenReturn(keyPairs); + HashicorpKeyVaultConfig hashicorpConfig = mock(HashicorpKeyVaultConfig.class); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(hashicorpConfig); + + assertThat(validator.isValid(keyConfiguration, context)).isFalse(); + verify(context).buildConstraintViolationWithTemplate("{ValidKeyVaultConfiguration.azure.message}"); + } + } diff --git a/config/src/test/java/com/quorum/tessera/config/constraints/PositiveIntegerValidatorTest.java b/config/src/test/java/com/quorum/tessera/config/constraints/PositiveIntegerValidatorTest.java new file mode 100644 index 0000000000..e0d489268b --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/constraints/PositiveIntegerValidatorTest.java @@ -0,0 +1,63 @@ +package com.quorum.tessera.config.constraints; + +import org.junit.Before; +import org.junit.Test; + +import javax.validation.ConstraintValidatorContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class PositiveIntegerValidatorTest { + + private PositiveIntegerValidator validator; + + private ConstraintValidatorContext context; + + @Before + public void setUp() { + this.validator = new PositiveIntegerValidator(); + this.context = mock(ConstraintValidatorContext.class); + } + + @Test + public void nullIsValid() { + assertThat(validator.isValid(null, context)).isTrue(); + } + + @Test + public void positiveIntegerIsValid() { + assertThat(validator.isValid("10", context)).isTrue(); + } + + @Test + public void zeroIsValid() { + assertThat(validator.isValid("0", context)).isTrue(); + } + + @Test + public void nonNumericCharactersAreInvalid() { + assertThat(validator.isValid("letters", context)).isFalse(); + } + + @Test + public void nonNumericCharactersFollowingNumbersIsInvalid() { + assertThat(validator.isValid("10letters", context)).isFalse(); + } + + @Test + public void decimalIsInvalid() { + assertThat(validator.isValid("1.0", context)).isFalse(); + } + + @Test + public void negativeIsInvalid() { + assertThat(validator.isValid("-10", context)).isFalse(); + } + + @Test + public void initialize() { + ValidPositiveInteger constraintAnnotation = mock(ValidPositiveInteger.class); + validator.initialize(constraintAnnotation); + } +} diff --git a/config/src/test/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidatorTest.java b/config/src/test/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidatorTest.java index bba8e02bc6..21c56fbe6f 100644 --- a/config/src/test/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidatorTest.java +++ b/config/src/test/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidatorTest.java @@ -16,6 +16,9 @@ public class UnsupportedKeyPairValidatorTest { private ValidUnsupportedKeyPair validUnsupportedKeyPair; private ConstraintValidatorContext context; + private UnsupportedKeyPair keyPair; + + @Before public void setUp() { this.validator = new UnsupportedKeyPairValidator(); @@ -27,11 +30,13 @@ public void setUp() { ConstraintValidatorContext.ConstraintViolationBuilder builder = mock(ConstraintValidatorContext.ConstraintViolationBuilder.class); when(context.buildConstraintViolationWithTemplate(any(String.class))).thenReturn(builder); + + this.keyPair = new UnsupportedKeyPair(); } @Test public void directViolationIfPublicKeyButNoPrivateKey() { - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, "public", null, null, null, null); + keyPair.setPublicKey("public"); validator.isValid(keyPair, context); @@ -40,7 +45,7 @@ public void directViolationIfPublicKeyButNoPrivateKey() { @Test public void directViolationIfNoPublicKeyButPrivateKey() { - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, "private", null, null, null, null, null); + keyPair.setPrivateKey("private"); validator.isValid(keyPair, context); @@ -52,7 +57,11 @@ public void directViolationIsDefaultIfNoDirectPublicEvenIfMultipleIncompleteKeyP KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, "private", null, path, null, null, "privVault"); + keyPair.setPrivateKey("private"); + keyPair.setConfig(keyDataConfig); + keyPair.setPrivateKeyPath(path); + keyPair.setAzureVaultPrivateKeyId("privAzure"); + keyPair.setHashicorpVaultPrivateKeyId("privHashicorp"); validator.isValid(keyPair, context); @@ -64,7 +73,11 @@ public void directViolationIsDefaultIfNoDirectPrivateEvenIfMultipleIncompleteKey KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, null, "public", null, path, "pubVault", null); + keyPair.setConfig(keyDataConfig); + keyPair.setPublicKey("public"); + keyPair.setPublicKeyPath(path); + keyPair.setAzureVaultPublicKeyId("pubAzure"); + keyPair.setHashicorpVaultPublicKeyId("pubHashicorp"); validator.isValid(keyPair, context); @@ -74,7 +87,8 @@ public void directViolationIsDefaultIfNoDirectPrivateEvenIfMultipleIncompleteKey @Test public void inlineViolationIfPrivateKeyConfigButNoPublicKey() { KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, null, null, null, null, null, null); + + keyPair.setConfig(keyDataConfig); validator.isValid(keyPair, context); @@ -86,7 +100,10 @@ public void inlineViolationIfNoPublicEvenIfVaultAndFilesystemAreIncomplete() { KeyDataConfig keyDataConfig = mock(KeyDataConfig.class); Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(keyDataConfig, null, null, null, path, "pubId", null); + keyPair.setConfig(keyDataConfig); + keyPair.setPublicKeyPath(path); + keyPair.setAzureVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultPublicKeyId("pubId"); validator.isValid(keyPair, context); @@ -95,7 +112,7 @@ public void inlineViolationIfNoPublicEvenIfVaultAndFilesystemAreIncomplete() { @Test public void azureViolationIfPublicIdButNoPrivateId() { - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, null, "pubId", null); + keyPair.setAzureVaultPublicKeyId("pubId"); validator.isValid(keyPair, context); @@ -104,7 +121,7 @@ public void azureViolationIfPublicIdButNoPrivateId() { @Test public void azureViolationIfNoPublicIdButPrivateId() { - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, null, null, "privId"); + keyPair.setAzureVaultPrivateKeyId("privId"); validator.isValid(keyPair, context); @@ -115,18 +132,160 @@ public void azureViolationIfNoPublicIdButPrivateId() { public void azureViolationIfNoPublicIdEvenIfFilesystemIncomplete() { Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, path, null, "privId"); + keyPair.setPublicKeyPath(path); + keyPair.setAzureVaultPrivateKeyId("privId"); validator.isValid(keyPair, context); verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.bothAzureKeysRequired.message}"); } + @Test + public void hashicorpViolationIfPublicIdOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPrivateIdOnly() { + keyPair.setHashicorpVaultPrivateKeyId("privId"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfSecretEngineNameOnly() { + keyPair.setHashicorpVaultSecretEngineName("secretEngineName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfSecretNameOnly() { + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndPrivateIdOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultPrivateKeyId("privId"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndSecretEngineNameOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndSecretNameOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPrivateIdAndSecretEngineNameOnly() { + keyPair.setHashicorpVaultPrivateKeyId("privId"); + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPrivateIdAndSecretNameOnly() { + keyPair.setHashicorpVaultPrivateKeyId("privId"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfSecretEngineNameAndSecretNameOnly() { + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndPrivateIdAndSecretEngineNameOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultPrivateKeyId("privId"); + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndPrivateIdAndSecretNameOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultPrivateKeyId("privId"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPublicIdAndSecretEngineNameAndSecretNameOnly() { + keyPair.setHashicorpVaultPublicKeyId("pubId"); + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + + @Test + public void hashicorpViolationIfPrivateIdAndSecretEngineNameAndSecretNameOnly() { + keyPair.setHashicorpVaultPrivateKeyId("privId"); + keyPair.setHashicorpVaultSecretEngineName("secretEngine"); + keyPair.setHashicorpVaultSecretName("secretName"); + + validator.isValid(keyPair, context); + + verify(context).buildConstraintViolationWithTemplate("{UnsupportedKeyPair.allHashicorpKeyDataRequired.message}"); + } + @Test public void azureViolationIfNoPrivateIdEvenIfFilesystemIncomplete() { Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, path, "pubId", null); + keyPair.setAzureVaultPublicKeyId("pubId"); + keyPair.setPublicKeyPath(path); validator.isValid(keyPair, context); @@ -137,7 +296,7 @@ public void azureViolationIfNoPrivateIdEvenIfFilesystemIncomplete() { public void filesystemViolationIfPublicPathButNoPrivatePath() { Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, path, null, null); + keyPair.setPublicKeyPath(path); validator.isValid(keyPair, context); @@ -148,7 +307,7 @@ public void filesystemViolationIfPublicPathButNoPrivatePath() { public void filesystemViolationIfNoPublicPathButPrivatePath() { Path path = mock(Path.class); - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, path, null, null, null); + keyPair.setPrivateKeyPath(path); validator.isValid(keyPair, context); @@ -157,8 +316,7 @@ public void filesystemViolationIfNoPublicPathButPrivatePath() { @Test public void defaultViolationIfNoRecognisedKeyPairDataProvided() { - UnsupportedKeyPair keyPair = new UnsupportedKeyPair(null, null, null, null, null, null, null); - + //nothing set validator.isValid(keyPair, context); verifyNoMoreInteractions(context); diff --git a/config/src/test/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPairTest.java b/config/src/test/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPairTest.java new file mode 100644 index 0000000000..6b3d10cc41 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/keypairs/HashicorpVaultKeyPairTest.java @@ -0,0 +1,49 @@ +package com.quorum.tessera.config.keypairs; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpVaultKeyPairTest { + + private HashicorpVaultKeyPair keyPair; + + @Before + public void setUp() { + keyPair = new HashicorpVaultKeyPair("pubId", "privId", "secretEngine", "secretName", "0"); + } + + @Test + public void getters() { + assertThat(keyPair.getPublicKeyId()).isEqualTo("pubId"); + assertThat(keyPair.getPrivateKeyId()).isEqualTo("privId"); + assertThat(keyPair.getSecretEngineName()).isEqualTo("secretEngine"); + assertThat(keyPair.getSecretName()).isEqualTo("secretName"); + assertThat(keyPair.getPublicKey()).isEqualTo(null); + assertThat(keyPair.getPrivateKey()).isEqualTo(null); + assertThat(keyPair.getPassword()).isEqualTo(""); + assertThat(keyPair.getSecretVersion()).isEqualTo("0"); + } + + @Test + public void getSecretVersionAsInt() { + keyPair = new HashicorpVaultKeyPair("pubId", "privId", "secretEngine", "secretName", "10"); + + assertThat(keyPair.getSecretVersionAsInt()).isEqualTo(10); + } + + @Test + public void getSecretVersionAsIntReturns0IfNull() { + keyPair = new HashicorpVaultKeyPair("pubId", "privId", "secretEngine", "secretName", null); + + assertThat(keyPair.getSecretVersionAsInt()).isEqualTo(0); + } + + @Test + public void withPasswordDoesNothing() { + assertThat(keyPair.getPassword()).isEqualTo(""); + keyPair.withPassword("newpwd"); + assertThat(keyPair.getPassword()).isEqualTo(""); + } +} diff --git a/config/src/test/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPairTest.java b/config/src/test/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPairTest.java index 74348c3eb7..3171ea9c42 100644 --- a/config/src/test/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPairTest.java +++ b/config/src/test/java/com/quorum/tessera/config/keypairs/UnsupportedKeyPairTest.java @@ -11,7 +11,7 @@ public class UnsupportedKeyPairTest { @Before public void setUp() { - this.keyPair = new UnsupportedKeyPair(null, null, null, null, null, null, null); + this.keyPair = new UnsupportedKeyPair(null, null, null, null, null, null, null, null, null, null, null, null); } @Test @@ -23,4 +23,13 @@ public void getPasswordAlwaysReturnsNull() { assertThat(keyPair.getPassword()).isNull(); } + @Test + public void setHashicorpVaultSecretVersion() { + assertThat(keyPair.getHashicorpVaultSecretVersion()).isNull(); + + keyPair.setHashicorpVaultSecretVersion("1"); + + assertThat(keyPair.getHashicorpVaultSecretVersion()).isEqualTo("1"); + } + } diff --git a/config/src/test/java/com/quorum/tessera/config/util/EnvironmentVariableProviderTest.java b/config/src/test/java/com/quorum/tessera/config/util/EnvironmentVariableProviderTest.java index cc7e8d51ef..dac0620e47 100644 --- a/config/src/test/java/com/quorum/tessera/config/util/EnvironmentVariableProviderTest.java +++ b/config/src/test/java/com/quorum/tessera/config/util/EnvironmentVariableProviderTest.java @@ -14,4 +14,12 @@ public void getEnv() { //returns null as env variables not set in test environment assertThat(provider.getEnv("env")).isNull(); } + + @Test + public void getEnvAsCharArray() { + EnvironmentVariableProvider provider = new EnvironmentVariableProvider(); + + //returns null as env variables not set in test environment + assertThat(provider.getEnvAsCharArray("env")).isNull(); + } } diff --git a/config/src/test/java/com/quorum/tessera/config/vault/data/AzureGetSecretDataTest.java b/config/src/test/java/com/quorum/tessera/config/vault/data/AzureGetSecretDataTest.java new file mode 100644 index 0000000000..9efeaabbdf --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/vault/data/AzureGetSecretDataTest.java @@ -0,0 +1,16 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AzureGetSecretDataTest { + @Test + public void getters() { + AzureGetSecretData data = new AzureGetSecretData("secretName"); + + assertThat(data.getSecretName()).isEqualTo("secretName"); + assertThat(data.getType()).isEqualTo(KeyVaultType.AZURE); + } +} diff --git a/config/src/test/java/com/quorum/tessera/config/vault/data/AzureSetSecretDataTest.java b/config/src/test/java/com/quorum/tessera/config/vault/data/AzureSetSecretDataTest.java new file mode 100644 index 0000000000..e6aae30695 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/vault/data/AzureSetSecretDataTest.java @@ -0,0 +1,17 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AzureSetSecretDataTest { + @Test + public void getters() { + AzureSetSecretData data = new AzureSetSecretData("secretName", "secret"); + + assertThat(data.getSecretName()).isEqualTo("secretName"); + assertThat(data.getSecret()).isEqualTo("secret"); + assertThat(data.getType()).isEqualTo(KeyVaultType.AZURE); + } +} diff --git a/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretDataTest.java b/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretDataTest.java new file mode 100644 index 0000000000..0358e958ed --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpGetSecretDataTest.java @@ -0,0 +1,31 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpGetSecretDataTest { + + private HashicorpGetSecretData getSecretData; + + @Before + public void setUp() { + this.getSecretData = new HashicorpGetSecretData("secret", "secretName", "keyId", 1); + } + + @Test + public void getters() { + assertThat(getSecretData.getSecretEngineName()).isEqualTo("secret"); + assertThat(getSecretData.getSecretName()).isEqualTo("secretName"); + assertThat(getSecretData.getValueId()).isEqualTo("keyId"); + assertThat(getSecretData.getSecretVersion()).isEqualTo(1); + } + + @Test + public void getType() { + assertThat(getSecretData.getType()).isEqualTo(KeyVaultType.HASHICORP); + } + +} diff --git a/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretDataTest.java b/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretDataTest.java new file mode 100644 index 0000000000..3f4b2d38e9 --- /dev/null +++ b/config/src/test/java/com/quorum/tessera/config/vault/data/HashicorpSetSecretDataTest.java @@ -0,0 +1,32 @@ +package com.quorum.tessera.config.vault.data; + +import com.quorum.tessera.config.KeyVaultType; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpSetSecretDataTest { + + private HashicorpSetSecretData setSecretData; + + @Before + public void setUp() { + this.setSecretData = new HashicorpSetSecretData("secret", "secretName", Collections.singletonMap("name", "value")); + } + + @Test + public void getters() { + assertThat(setSecretData.getSecretEngineName()).isEqualTo("secret"); + assertThat(setSecretData.getSecretName()).isEqualTo("secretName"); + assertThat(setSecretData.getNameValuePairs()).isEqualTo(Collections.singletonMap("name", "value")); + } + + @Test + public void getType() { + assertThat(setSecretData.getType()).isEqualTo(KeyVaultType.HASHICORP); + } + +} diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/AzureVaultKeyGenerator.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/AzureVaultKeyGenerator.java index 9c7e76471d..733747fe75 100644 --- a/key-generation/src/main/java/com/quorum/tessera/key/generation/AzureVaultKeyGenerator.java +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/AzureVaultKeyGenerator.java @@ -2,6 +2,8 @@ import com.quorum.tessera.config.ArgonOptions; import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; +import com.quorum.tessera.config.vault.data.AzureSetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; import com.quorum.tessera.encryption.Key; import com.quorum.tessera.encryption.KeyPair; import com.quorum.tessera.key.vault.KeyVaultService; @@ -26,7 +28,7 @@ public AzureVaultKeyGenerator(final NaclFacade nacl, KeyVaultService keyVaultSer } @Override - public AzureVaultKeyPair generate(String filename, ArgonOptions encryptionOptions) { + public AzureVaultKeyPair generate(String filename, ArgonOptions encryptionOptions, KeyVaultOptions keyVaultOptions) { final KeyPair keys = this.nacl.generateNewKeys(); final StringBuilder publicId = new StringBuilder(); @@ -55,7 +57,9 @@ public AzureVaultKeyPair generate(String filename, ArgonOptions encryptionOption } private void saveKeyInVault(String id, Key key) { - keyVaultService.setSecret(id, key.encodeToBase64()); + SetSecretData setSecretData = new AzureSetSecretData(id, key.encodeToBase64()); + + keyVaultService.setSecret(setSecretData); LOGGER.debug("Key {} saved to vault with id {}", key.encodeToBase64(), id); LOGGER.info("Key saved to vault with id {}", id); } diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java index bb00a1721a..9b738e0197 100644 --- a/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java @@ -1,9 +1,6 @@ package com.quorum.tessera.key.generation; -import com.quorum.tessera.config.AzureKeyVaultConfig; -import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.KeyConfiguration; -import com.quorum.tessera.config.KeyVaultConfig; +import com.quorum.tessera.config.*; import com.quorum.tessera.config.keys.KeyEncryptorFactory; import com.quorum.tessera.config.util.EnvironmentVariableProvider; import com.quorum.tessera.config.util.PasswordReaderFactory; @@ -21,12 +18,25 @@ public KeyGenerator create(KeyVaultConfig keyVaultConfig) { final Config config = new Config(); final KeyConfiguration keyConfiguration = new KeyConfiguration(); - keyConfiguration.setAzureKeyVaultConfig((AzureKeyVaultConfig)keyVaultConfig); - config.setKeys(keyConfiguration); - final KeyVaultService keyVaultService = keyVaultServiceFactory.create(config, new EnvironmentVariableProvider()); + if(keyVaultConfig.getKeyVaultType().equals(KeyVaultType.AZURE)) { + keyConfiguration.setAzureKeyVaultConfig((AzureKeyVaultConfig) keyVaultConfig); - return new AzureVaultKeyGenerator(NaclFacadeFactory.newFactory().create(), keyVaultService); + config.setKeys(keyConfiguration); + + final KeyVaultService keyVaultService = keyVaultServiceFactory.create(config, new EnvironmentVariableProvider()); + + return new AzureVaultKeyGenerator(NaclFacadeFactory.newFactory().create(), keyVaultService); + + } else { + keyConfiguration.setHashicorpKeyVaultConfig((HashicorpKeyVaultConfig) keyVaultConfig); + + config.setKeys(keyConfiguration); + + final KeyVaultService keyVaultService = keyVaultServiceFactory.create(config, new EnvironmentVariableProvider()); + + return new HashicorpVaultKeyGenerator(NaclFacadeFactory.newFactory().create(), keyVaultService); + } } return new FileKeyGenerator( diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/FileKeyGenerator.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/FileKeyGenerator.java index 2d9ba722d4..35c57bf3eb 100644 --- a/key-generation/src/main/java/com/quorum/tessera/key/generation/FileKeyGenerator.java +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/FileKeyGenerator.java @@ -5,8 +5,8 @@ import com.quorum.tessera.config.keys.KeyEncryptor; import com.quorum.tessera.config.util.JaxbUtil; import com.quorum.tessera.config.util.PasswordReader; -import com.quorum.tessera.io.IOCallback; import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.io.IOCallback; import com.quorum.tessera.nacl.NaclFacade; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +39,7 @@ public FileKeyGenerator(final NaclFacade nacl, final KeyEncryptor keyEncryptor, } @Override - public FilesystemKeyPair generate(final String filename, final ArgonOptions encryptionOptions) { + public FilesystemKeyPair generate(final String filename, final ArgonOptions encryptionOptions, final KeyVaultOptions keyVaultOptions) { final String password = this.passwordReader.requestUserPassword(); @@ -47,7 +47,7 @@ public FilesystemKeyPair generate(final String filename, final ArgonOptions encr final String publicKeyBase64 = Base64.getEncoder().encodeToString(generated.getPublicKey().getKeyBytes()); - final KeyData finalKeys; + final KeyData finalKeys = new KeyData(); if (!password.isEmpty()) { @@ -55,7 +55,7 @@ public FilesystemKeyPair generate(final String filename, final ArgonOptions encr generated.getPrivateKey(), password, encryptionOptions ); - finalKeys = new KeyData( + finalKeys.setConfig( new KeyDataConfig( new PrivateKeyData( null, @@ -66,13 +66,7 @@ public FilesystemKeyPair generate(final String filename, final ArgonOptions encr null ), PrivateKeyType.LOCKED - ), - generated.getPrivateKey().toString(), - publicKeyBase64, - null, - null, - null, - null + ) ); LOGGER.info("Newly generated private key has been encrypted"); @@ -80,22 +74,19 @@ public FilesystemKeyPair generate(final String filename, final ArgonOptions encr } else { String keyData = Base64.getEncoder().encodeToString(generated.getPrivateKey().getKeyBytes()); - - finalKeys = new KeyData( + + finalKeys.setConfig( new KeyDataConfig( new PrivateKeyData(keyData, null, null, null, null, null), PrivateKeyType.UNLOCKED - ), - generated.getPrivateKey().toString(), - publicKeyBase64, - null, - null, - null, - null + ) ); } + finalKeys.setPrivateKey(generated.getPrivateKey().encodeToBase64()); + finalKeys.setPublicKey(publicKeyBase64); + final String privateKeyJson = JaxbUtil.marshalToString(finalKeys.getConfig()); final Path resolvedPath = Paths.get(filename).toAbsolutePath(); diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGenerator.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGenerator.java new file mode 100644 index 0000000000..e7defba913 --- /dev/null +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGenerator.java @@ -0,0 +1,52 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.ArgonOptions; +import com.quorum.tessera.config.keypairs.HashicorpVaultKeyPair; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; +import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.nacl.NaclFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class HashicorpVaultKeyGenerator implements KeyGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger(HashicorpVaultKeyGenerator.class); + + private final NaclFacade nacl; + private final KeyVaultService keyVaultService; + + public HashicorpVaultKeyGenerator(final NaclFacade nacl, KeyVaultService keyVaultService) { + this.nacl = nacl; + this.keyVaultService = keyVaultService; + } + + @Override + public HashicorpVaultKeyPair generate(String filename, ArgonOptions encryptionOptions, KeyVaultOptions keyVaultOptions) { + Objects.requireNonNull(filename); + Objects.requireNonNull(keyVaultOptions, "-keygenvaultsecretengine must be provided if using the Hashicorp vault type"); + Objects.requireNonNull(keyVaultOptions.getSecretEngineName(), "-keygenvaultsecretengine must be provided if using the Hashicorp vault type"); + + final KeyPair keys = this.nacl.generateNewKeys(); + + String pubId = "publicKey"; + String privId = "privateKey"; + Map keyPairData = new HashMap<>(); + keyPairData.put(pubId, keys.getPublicKey().encodeToBase64()); + keyPairData.put(privId, keys.getPrivateKey().encodeToBase64()); + + SetSecretData setSecretData = new HashicorpSetSecretData(keyVaultOptions.getSecretEngineName(), filename, keyPairData); + + keyVaultService.setSecret(setSecretData); + LOGGER.debug("Key {} saved to vault secret engine {} with name {} and id {}", keyPairData.get(pubId), keyVaultOptions.getSecretEngineName(), filename, pubId); + LOGGER.info("Key saved to vault secret engine {} with name {} and id {}", keyVaultOptions.getSecretEngineName(), filename, pubId); + LOGGER.debug("Key {} saved to vault secret engine {} with name {} and id {}", keyPairData.get(privId), keyVaultOptions.getSecretEngineName(), filename, privId); + LOGGER.info("Key saved to vault secret engine {} with name {} and id {}", keyVaultOptions.getSecretEngineName(), filename, privId); + + return new HashicorpVaultKeyPair(pubId, privId, keyVaultOptions.getSecretEngineName(), filename, null); + } +} diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyGenerator.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyGenerator.java index 1b95d8b30c..c2ba39e6f7 100644 --- a/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyGenerator.java +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyGenerator.java @@ -5,6 +5,6 @@ public interface KeyGenerator { - ConfigKeyPair generate(String filename, ArgonOptions encryptionOptions); + ConfigKeyPair generate(String filename, ArgonOptions encryptionOptions, KeyVaultOptions keyVaultOptions); } diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyVaultOptions.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyVaultOptions.java new file mode 100644 index 0000000000..738c879eb5 --- /dev/null +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/KeyVaultOptions.java @@ -0,0 +1,13 @@ +package com.quorum.tessera.key.generation; + +public class KeyVaultOptions { + private String secretEngineName; + + public KeyVaultOptions(String secretEngineName) { + this.secretEngineName = secretEngineName; + } + + public String getSecretEngineName() { + return secretEngineName; + } +} diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/AzureVaultKeyGeneratorTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/AzureVaultKeyGeneratorTest.java index 9077597787..a6fa763f0f 100644 --- a/key-generation/src/test/java/com/quorum/tessera/key/generation/AzureVaultKeyGeneratorTest.java +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/AzureVaultKeyGeneratorTest.java @@ -2,6 +2,7 @@ import com.quorum.tessera.config.ArgonOptions; import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; +import com.quorum.tessera.config.vault.data.AzureSetSecretData; import com.quorum.tessera.encryption.KeyPair; import com.quorum.tessera.encryption.PrivateKey; import com.quorum.tessera.encryption.PublicKey; @@ -9,8 +10,10 @@ import com.quorum.tessera.nacl.NaclFacade; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import java.nio.charset.UnsupportedCharsetException; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -40,16 +43,25 @@ public void setUp() { } @Test - public void keysSavedInVaultWithProvidedVaultId() { + public void keysSavedInVaultWithProvidedVaultIdAndCorrectSuffix() { final String vaultId = "vaultId"; final String pubVaultId = vaultId + "Pub"; final String privVaultId = vaultId + "Key"; - final AzureVaultKeyPair result = azureVaultKeyGenerator.generate(vaultId, null); + final AzureVaultKeyPair result = azureVaultKeyGenerator.generate(vaultId, null, null); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AzureSetSecretData.class); + + verify(keyVaultService, times(2)).setSecret(captor.capture()); + + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); + + AzureSetSecretData expectedDataPub = new AzureSetSecretData(pubVaultId, pub.encodeToBase64()); + AzureSetSecretData expectedDataPriv = new AzureSetSecretData(privVaultId, priv.encodeToBase64()); + + assertThat(capturedArgs).usingRecursiveFieldByFieldElementComparator().containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); - verify(keyVaultService, times(2)).setSecret(any(String.class), any(String.class)); - verify(keyVaultService, times(1)).setSecret(vaultId + "Pub", pub.encodeToBase64()); - verify(keyVaultService, times(1)).setSecret(vaultId + "Key", priv.encodeToBase64()); verifyNoMoreInteractions(keyVaultService); final AzureVaultKeyPair expected = new AzureVaultKeyPair(pubVaultId, privVaultId); @@ -58,49 +70,55 @@ public void keysSavedInVaultWithProvidedVaultId() { } @Test - public void publicKeyIsSavedToVaultAndIdHasPubSuffix() { + public void vaultIdIsFinalComponentOfFilePath() { final String vaultId = "vaultId"; + final String pubVaultId = vaultId + "Pub"; + final String privVaultId = vaultId + "Key"; + final String path = "/some/path/" + vaultId; - azureVaultKeyGenerator.generate(vaultId, null); + azureVaultKeyGenerator.generate(path, null, null); - verify(keyVaultService, times(1)).setSecret(vaultId + "Pub", pub.encodeToBase64()); - } + final ArgumentCaptor captor = ArgumentCaptor.forClass(AzureSetSecretData.class); - @Test - public void privateKeyIsSavedToVaultAndIdHasKeySuffix() { - final String vaultId = "vaultId"; + verify(keyVaultService, times(2)).setSecret(captor.capture()); - azureVaultKeyGenerator.generate(vaultId, null); + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); - verify(keyVaultService, times(1)).setSecret(vaultId + "Key", priv.encodeToBase64()); - } + AzureSetSecretData expectedDataPub = new AzureSetSecretData(pubVaultId, pub.encodeToBase64()); + AzureSetSecretData expectedDataPriv = new AzureSetSecretData(privVaultId, priv.encodeToBase64()); - @Test - public void vaultIdIsFinalComponentOfFilePath() { - final String vaultId = "vaultId"; - final String path = "/some/path/" + vaultId; - - azureVaultKeyGenerator.generate(path, null); + assertThat(capturedArgs).usingRecursiveFieldByFieldElementComparator().containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); - verify(keyVaultService, times(1)).setSecret(vaultId + "Pub", pub.encodeToBase64()); - verify(keyVaultService, times(1)).setSecret(vaultId + "Key", priv.encodeToBase64()); + verifyNoMoreInteractions(keyVaultService); } @Test public void ifNoVaultIdProvidedThenSuffixOnlyIsUsed() { - azureVaultKeyGenerator.generate(null, null); + azureVaultKeyGenerator.generate(null, null, null); - verify(keyVaultService, times(1)).setSecret("Pub", pub.encodeToBase64()); - verify(keyVaultService, times(1)).setSecret("Key", priv.encodeToBase64()); + final ArgumentCaptor captor = ArgumentCaptor.forClass(AzureSetSecretData.class); + + verify(keyVaultService, times(2)).setSecret(captor.capture()); + + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); + + AzureSetSecretData expectedDataPub = new AzureSetSecretData("Pub", pub.encodeToBase64()); + AzureSetSecretData expectedDataPriv = new AzureSetSecretData("Key", priv.encodeToBase64()); + + assertThat(capturedArgs).usingRecursiveFieldByFieldElementComparator().containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); + + verifyNoMoreInteractions(keyVaultService); } @Test public void allowedCharactersUsedInVaultIdDoesNotThrowException() { final String allowedId = "abcdefghijklmnopqrstuvwxyz-ABCDEFDGHIJKLMNOPQRSTUVWXYZ-0123456789"; - azureVaultKeyGenerator.generate(allowedId, null); + azureVaultKeyGenerator.generate(allowedId, null, null); - verify(keyVaultService, times(2)).setSecret(any(String.class), any(String.class)); + verify(keyVaultService, times(2)).setSecret(any(AzureSetSecretData.class)); } @Test @@ -108,7 +126,7 @@ public void exceptionThrownIfDisallowedCharactersUsedInVaultId() { final String invalidId = "/tmp/abc@+"; final Throwable throwable = catchThrowable( - () -> azureVaultKeyGenerator.generate(invalidId, null) + () -> azureVaultKeyGenerator.generate(invalidId, null, null) ); assertThat(throwable).isInstanceOf(UnsupportedCharsetException.class); @@ -121,7 +139,7 @@ public void exceptionThrownIfDisallowedCharactersUsedInVaultId() { public void encryptionIsNotUsedWhenSavingToVault() { final ArgonOptions argonOptions = mock(ArgonOptions.class); - azureVaultKeyGenerator.generate("vaultId", argonOptions); + azureVaultKeyGenerator.generate("vaultId", argonOptions, null); verifyNoMoreInteractions(argonOptions); } diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/FileKeyGeneratorTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/FileKeyGeneratorTest.java index 7a9820cbad..c1cf4f6995 100644 --- a/key-generation/src/test/java/com/quorum/tessera/key/generation/FileKeyGeneratorTest.java +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/FileKeyGeneratorTest.java @@ -8,6 +8,8 @@ import com.quorum.tessera.config.keys.KeyEncryptor; import com.quorum.tessera.config.util.PasswordReader; import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.encryption.PrivateKey; +import com.quorum.tessera.encryption.PublicKey; import com.quorum.tessera.nacl.NaclFacade; import org.junit.After; import org.junit.Before; @@ -21,8 +23,6 @@ import java.util.UUID; import static com.quorum.tessera.config.PrivateKeyType.UNLOCKED; -import com.quorum.tessera.encryption.PrivateKey; -import com.quorum.tessera.encryption.PublicKey; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -74,7 +74,7 @@ public void generateFromKeyDataUnlockedPrivateKey() throws IOException { String filename = UUID.randomUUID().toString(); - final FilesystemKeyPair generated = generator.generate(filename, null); + final FilesystemKeyPair generated = generator.generate(filename, null, null); assertThat(generated).isInstanceOf(FilesystemKeyPair.class); assertThat(generated.getPublicKey()).isEqualTo("cHVibGljS2V5"); @@ -114,7 +114,7 @@ public void generateFromKeyDataLockedPrivateKey() throws IOException { doReturn(encryptedKey).when(keyEncryptor).encryptPrivateKey(any(PrivateKey.class), anyString(), eq(null)); - final FilesystemKeyPair generated = generator.generate(keyFilesName, null); + final FilesystemKeyPair generated = generator.generate(keyFilesName, null, null); final KeyDataConfig pkd = generated.getInlineKeypair().getPrivateKeyConfig(); assertThat(generated.getPublicKey()).isEqualTo("cHVibGljS2V5"); @@ -134,7 +134,7 @@ public void providingPathSavesToFile() throws IOException { doReturn(keyPair).when(nacl).generateNewKeys(); - final FilesystemKeyPair generated = generator.generate(keyFilesName, null); + final FilesystemKeyPair generated = generator.generate(keyFilesName, null, null); assertThat(Files.exists(tempFolder.resolve("providingPathSavesToFile.pub"))).isTrue(); assertThat(Files.exists(tempFolder.resolve("providingPathSavesToFile.key"))).isTrue(); @@ -149,7 +149,7 @@ public void providingNoPathSavesToFileInSameDirectory() throws IOException { doReturn(keyPair).when(nacl).generateNewKeys(); - final FilesystemKeyPair generated = generator.generate("", null); + final FilesystemKeyPair generated = generator.generate("", null, null); assertThat(Files.exists(Paths.get(".pub"))).isTrue(); assertThat(Files.exists(Paths.get(".key"))).isTrue(); @@ -172,7 +172,7 @@ public void providingPathThatExistsThrowsError() throws IOException { .when(keyEncryptor) .encryptPrivateKey(any(PrivateKey.class), anyString(), eq(null)); - final Throwable throwable = catchThrowable(() -> generator.generate(keyFilesName, null)); + final Throwable throwable = catchThrowable(() -> generator.generate(keyFilesName, null, null)); assertThat(throwable).isInstanceOf(UncheckedIOException.class); diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGeneratorTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGeneratorTest.java new file mode 100644 index 0000000000..aa302f860a --- /dev/null +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/HashicorpVaultKeyGeneratorTest.java @@ -0,0 +1,95 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.keypairs.HashicorpVaultKeyPair; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.encryption.PrivateKey; +import com.quorum.tessera.encryption.PublicKey; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.nacl.NaclFacade; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class HashicorpVaultKeyGeneratorTest { + + private final String pubStr = "public"; + private final String privStr = "private"; + private final PublicKey pub = PublicKey.from(pubStr.getBytes()); + private final PrivateKey priv = PrivateKey.from(privStr.getBytes()); + + private NaclFacade naclFacade; + private KeyVaultService keyVaultService; + private HashicorpVaultKeyGenerator hashicorpVaultKeyGenerator; + + @Before + public void setUp() { + this.naclFacade = mock(NaclFacade.class); + this.keyVaultService = mock(KeyVaultService.class); + + final KeyPair keyPair = new KeyPair(pub, priv); + when(naclFacade.generateNewKeys()).thenReturn(keyPair); + + this.hashicorpVaultKeyGenerator = new HashicorpVaultKeyGenerator(naclFacade, keyVaultService); + + } + + @Test(expected = NullPointerException.class) + public void nullFilenameThrowsException() { + KeyVaultOptions keyVaultOptions = mock(KeyVaultOptions.class); + when(keyVaultOptions.getSecretEngineName()).thenReturn("secretEngine"); + + hashicorpVaultKeyGenerator.generate(null, null, keyVaultOptions); + } + + @Test(expected = NullPointerException.class) + public void nullKeyVaultOptionsThrowsException() { + hashicorpVaultKeyGenerator.generate("filename", null, null); + } + + @Test(expected = NullPointerException.class) + public void nullSecretEngineNameThrowsException() { + KeyVaultOptions keyVaultOptions = mock(KeyVaultOptions.class); + when(keyVaultOptions.getSecretEngineName()).thenReturn(null); + + hashicorpVaultKeyGenerator.generate("filename", null, keyVaultOptions); + } + + @Test + public void generatedKeyPairIsSavedToSpecifiedPathInVaultWithIds() { + String secretEngine = "secretEngine"; + String filename = "secretName"; + + KeyVaultOptions keyVaultOptions = mock(KeyVaultOptions.class); + when(keyVaultOptions.getSecretEngineName()).thenReturn(secretEngine); + + HashicorpVaultKeyPair result = hashicorpVaultKeyGenerator.generate(filename, null, keyVaultOptions); + + HashicorpVaultKeyPair expected = new HashicorpVaultKeyPair("publicKey", "privateKey", secretEngine, filename, null); + assertThat(result).isEqualToComparingFieldByField(expected); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(HashicorpSetSecretData.class); + verify(keyVaultService).setSecret(captor.capture()); + + assertThat(captor.getAllValues()).hasSize(1); + HashicorpSetSecretData capturedArg = captor.getValue(); + + Map expectedNameValuePairs = new HashMap<>(); + expectedNameValuePairs.put("publicKey", pub.encodeToBase64()); + expectedNameValuePairs.put("privateKey", priv.encodeToBase64()); + + HashicorpSetSecretData expectedData = new HashicorpSetSecretData(secretEngine, filename, expectedNameValuePairs); + + assertThat(capturedArg).isEqualToComparingFieldByFieldRecursively(expectedData); + + verifyNoMoreInteractions(keyVaultService); + + } + +} diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java index 085f52ad06..54bc7fae77 100644 --- a/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java @@ -1,6 +1,7 @@ package com.quorum.tessera.key.generation; import com.quorum.tessera.config.AzureKeyVaultConfig; +import com.quorum.tessera.config.HashicorpKeyVaultConfig; import com.quorum.tessera.config.util.EnvironmentVariableProvider; import org.junit.Test; @@ -22,8 +23,8 @@ public void fileKeyGeneratorWhenKeyVaultConfigNotProvided() { } @Test - public void azureVaultKeyGeneratorWhenKeyVaultConfigProvided() { - final AzureKeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig("url"); + public void azureVaultKeyGeneratorWhenAzureConfigProvided() { + final AzureKeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig(); final KeyGenerator keyGenerator = KeyGeneratorFactory.newFactory().create(keyVaultConfig); @@ -31,4 +32,16 @@ public void azureVaultKeyGeneratorWhenKeyVaultConfigProvided() { assertThat(keyGenerator).isExactlyInstanceOf(AzureVaultKeyGenerator.class); } + @Test + public void hashicorpVaultKeyGeneratorWhenHashicorpConfigProvided() { + final HashicorpKeyVaultConfig keyVaultConfig = new HashicorpKeyVaultConfig(); + + final KeyGenerator keyGenerator = KeyGeneratorFactory.newFactory().create(keyVaultConfig); + + assertThat(keyGenerator).isNotNull(); + assertThat(keyGenerator).isExactlyInstanceOf(HashicorpVaultKeyGenerator.class); + } + + + } diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyVaultOptionsTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyVaultOptionsTest.java new file mode 100644 index 0000000000..9486507349 --- /dev/null +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyVaultOptionsTest.java @@ -0,0 +1,18 @@ +package com.quorum.tessera.key.generation; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KeyVaultOptionsTest { + + @Test + public void getters() { + String secretEngineName = "secretEngineName"; + + KeyVaultOptions keyVaultOptions = new KeyVaultOptions(secretEngineName); + + assertThat(keyVaultOptions.getSecretEngineName()).isEqualTo(secretEngineName); + } + +} diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAzureKeyVaultServiceFactory.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAzureKeyVaultServiceFactory.java index 93343332c7..da258e4b7f 100644 --- a/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAzureKeyVaultServiceFactory.java +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAzureKeyVaultServiceFactory.java @@ -3,9 +3,11 @@ import com.quorum.tessera.config.Config; import com.quorum.tessera.config.KeyVaultType; import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AzureGetSecretData; import com.quorum.tessera.key.vault.KeyVaultService; import com.quorum.tessera.key.vault.KeyVaultServiceFactory; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -13,8 +15,10 @@ public class MockAzureKeyVaultServiceFactory implements KeyVaultServiceFactory { @Override public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { KeyVaultService mock = mock(KeyVaultService.class); - when(mock.getSecret("pub")).thenReturn("publicSecret"); - when(mock.getSecret("priv")).thenReturn("privSecret"); + + when(mock.getSecret(any(AzureGetSecretData.class))) + .thenReturn("publicSecret") + .thenReturn("privSecret"); return mock; } diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/MockHashicorpKeyVaultServiceFactory.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockHashicorpKeyVaultServiceFactory.java new file mode 100644 index 0000000000..2585c24bb9 --- /dev/null +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockHashicorpKeyVaultServiceFactory.java @@ -0,0 +1,31 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.KeyVaultType; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockHashicorpKeyVaultServiceFactory implements KeyVaultServiceFactory { + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + KeyVaultService mock = mock(KeyVaultService.class); + + when(mock.getSecret(any(HashicorpGetSecretData.class))) + .thenReturn("publicSecret") + .thenReturn("privSecret"); + + return mock; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.HASHICORP; + } +} + diff --git a/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory index bf914724f3..ec7218aba2 100644 --- a/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory +++ b/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -1 +1,2 @@ -com.quorum.tessera.key.generation.MockAzureKeyVaultServiceFactory \ No newline at end of file +com.quorum.tessera.key.generation.MockAzureKeyVaultServiceFactory +com.quorum.tessera.key.generation.MockHashicorpKeyVaultServiceFactory \ No newline at end of file diff --git a/key-pair-converter/src/main/java/com/quorum/tessera/keypairconverter/KeyPairConverter.java b/key-pair-converter/src/main/java/com/quorum/tessera/keypairconverter/KeyPairConverter.java index a519daf5f4..90b94d970b 100644 --- a/key-pair-converter/src/main/java/com/quorum/tessera/keypairconverter/KeyPairConverter.java +++ b/key-pair-converter/src/main/java/com/quorum/tessera/keypairconverter/KeyPairConverter.java @@ -5,7 +5,11 @@ import com.quorum.tessera.config.KeyVaultType; import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; import com.quorum.tessera.config.keypairs.ConfigKeyPair; +import com.quorum.tessera.config.keypairs.HashicorpVaultKeyPair; import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AzureGetSecretData; +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; import com.quorum.tessera.encryption.KeyPair; import com.quorum.tessera.encryption.PrivateKey; import com.quorum.tessera.encryption.PublicKey; @@ -40,14 +44,34 @@ private KeyPair convert(ConfigKeyPair configKeyPair) { String base64PrivateKey; if(configKeyPair instanceof AzureVaultKeyPair) { + KeyVaultServiceFactory keyVaultServiceFactory = KeyVaultServiceFactory.getInstance(KeyVaultType.AZURE); + KeyVaultService keyVaultService = keyVaultServiceFactory.create(config, envProvider); + AzureVaultKeyPair akp = (AzureVaultKeyPair) configKeyPair; - base64PublicKey = keyVaultService.getSecret(akp.getPublicKeyId()); - base64PrivateKey = keyVaultService.getSecret(akp.getPrivateKeyId()); + GetSecretData getPublicKeyData = new AzureGetSecretData(akp.getPublicKeyId()); + GetSecretData getPrivateKeyData = new AzureGetSecretData(akp.getPrivateKeyId()); + + base64PublicKey = keyVaultService.getSecret(getPublicKeyData); + base64PrivateKey = keyVaultService.getSecret(getPrivateKeyData); + } + else if(configKeyPair instanceof HashicorpVaultKeyPair) { + + KeyVaultServiceFactory keyVaultServiceFactory = KeyVaultServiceFactory.getInstance(KeyVaultType.HASHICORP); - } else { + KeyVaultService keyVaultService = keyVaultServiceFactory.create(config, envProvider); + + HashicorpVaultKeyPair hkp = (HashicorpVaultKeyPair) configKeyPair; + + GetSecretData getPublicKeyData = new HashicorpGetSecretData(hkp.getSecretEngineName(), hkp.getSecretName(), hkp.getPublicKeyId(), hkp.getSecretVersionAsInt()); + GetSecretData getPrivateKeyData = new HashicorpGetSecretData(hkp.getSecretEngineName(), hkp.getSecretName(), hkp.getPrivateKeyId(), hkp.getSecretVersionAsInt()); + + base64PublicKey = keyVaultService.getSecret(getPublicKeyData); + base64PrivateKey = keyVaultService.getSecret(getPrivateKeyData); + } + else { base64PublicKey = configKeyPair.getPublicKey(); base64PrivateKey = configKeyPair.getPrivateKey(); diff --git a/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/KeyPairConverterTest.java b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/KeyPairConverterTest.java index fe5f434fb2..0214e0ed4f 100644 --- a/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/KeyPairConverterTest.java +++ b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/KeyPairConverterTest.java @@ -1,10 +1,7 @@ package com.quorum.tessera.keypairconverter; import com.quorum.tessera.config.Config; -import com.quorum.tessera.config.keypairs.AzureVaultKeyPair; -import com.quorum.tessera.config.keypairs.DirectKeyPair; -import com.quorum.tessera.config.keypairs.FilesystemKeyPair; -import com.quorum.tessera.config.keypairs.InlineKeypair; +import com.quorum.tessera.config.keypairs.*; import com.quorum.tessera.config.util.EnvironmentVariableProvider; import com.quorum.tessera.encryption.KeyPair; import com.quorum.tessera.encryption.PrivateKey; @@ -95,6 +92,19 @@ public void convertSingleAzureVaultKeyPair() { assertThat(resultKeyPair).isEqualToComparingFieldByField(expected); } + @Test + public void convertSingleHashicorpVaultKeyPair() { + final HashicorpVaultKeyPair keyPair = new HashicorpVaultKeyPair("pub", "priv", "engine", "secretName", "10"); + + Collection result = converter.convert(Collections.singletonList(keyPair)); + + assertThat(result).hasSize(1); + + KeyPair resultKeyPair = result.iterator().next(); + KeyPair expected = new KeyPair(PublicKey.from(decodeBase64("publicSecret")), PrivateKey.from(decodeBase64("privSecret"))); + + assertThat(resultKeyPair).isEqualToComparingFieldByField(expected); + } @Test public void convertMultipleKeyPairs() { diff --git a/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockAzureKeyVaultServiceFactory.java b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockAzureKeyVaultServiceFactory.java index 707cff3ced..bb03726f06 100644 --- a/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockAzureKeyVaultServiceFactory.java +++ b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockAzureKeyVaultServiceFactory.java @@ -3,9 +3,11 @@ import com.quorum.tessera.config.Config; import com.quorum.tessera.config.KeyVaultType; import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AzureGetSecretData; import com.quorum.tessera.key.vault.KeyVaultService; import com.quorum.tessera.key.vault.KeyVaultServiceFactory; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -13,8 +15,10 @@ public class MockAzureKeyVaultServiceFactory implements KeyVaultServiceFactory { @Override public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { KeyVaultService mock = mock(KeyVaultService.class); - when(mock.getSecret("pub")).thenReturn("publicSecret"); - when(mock.getSecret("priv")).thenReturn("privSecret"); + + when(mock.getSecret(any(AzureGetSecretData.class))) + .thenReturn("publicSecret") + .thenReturn("privSecret"); return mock; } diff --git a/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockHashicorpKeyVaultServiceFactory.java b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockHashicorpKeyVaultServiceFactory.java new file mode 100644 index 0000000000..690df05511 --- /dev/null +++ b/key-pair-converter/src/test/java/com/quorum/tessera/keypairconverter/MockHashicorpKeyVaultServiceFactory.java @@ -0,0 +1,30 @@ +package com.quorum.tessera.keypairconverter; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.KeyVaultType; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockHashicorpKeyVaultServiceFactory implements KeyVaultServiceFactory { + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + KeyVaultService mock = mock(KeyVaultService.class); + + when(mock.getSecret(any(HashicorpGetSecretData.class))) + .thenReturn("publicSecret") + .thenReturn("privSecret"); + + return mock; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.HASHICORP; + } +} diff --git a/key-pair-converter/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/key-pair-converter/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory index 469af955cc..f614cc09d2 100644 --- a/key-pair-converter/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory +++ b/key-pair-converter/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -1 +1,2 @@ -com.quorum.tessera.keypairconverter.MockAzureKeyVaultServiceFactory \ No newline at end of file +com.quorum.tessera.keypairconverter.MockAzureKeyVaultServiceFactory +com.quorum.tessera.keypairconverter.MockHashicorpKeyVaultServiceFactory \ No newline at end of file diff --git a/key-vault/azure-key-vault/pom.xml b/key-vault/azure-key-vault/pom.xml index 780c072edc..b39277602f 100644 --- a/key-vault/azure-key-vault/pom.xml +++ b/key-vault/azure-key-vault/pom.xml @@ -19,13 +19,11 @@ com.microsoft.azure azure-keyvault - 1.1.2 com.microsoft.azure adal4j - 1.6.3 diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureCredentialNotSetException.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureCredentialNotSetException.java index 43d2f61fd6..09afd5880a 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureCredentialNotSetException.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureCredentialNotSetException.java @@ -1,8 +1,8 @@ package com.quorum.tessera.key.vault.azure; -public class AzureCredentialNotSetException extends IllegalStateException { +class AzureCredentialNotSetException extends IllegalStateException { - public AzureCredentialNotSetException(String message) { + AzureCredentialNotSetException(String message) { super(message); } diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientCredentials.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientCredentials.java index 360125fd44..11bbd0f663 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientCredentials.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientCredentials.java @@ -24,13 +24,13 @@ public class AzureKeyVaultClientCredentials extends KeyVaultCredentials { private final ExecutorService executorService; - public AzureKeyVaultClientCredentials(String clientId, String clientSecret, ExecutorService executorService) { + AzureKeyVaultClientCredentials(String clientId, String clientSecret, ExecutorService executorService) { this.clientId = clientId; this.clientSecret = clientSecret; this.executorService = Objects.requireNonNull(executorService); } - protected void setAuthenticationContext(AuthenticationContext authenticationContext) { + void setAuthenticationContext(AuthenticationContext authenticationContext) { this.authenticationContext = authenticationContext; } diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientDelegate.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientDelegate.java index 63851463d4..c12b92d9ed 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientDelegate.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientDelegate.java @@ -6,18 +6,18 @@ import java.util.Objects; -public class AzureKeyVaultClientDelegate { +class AzureKeyVaultClientDelegate { private final KeyVaultClient keyVaultClient; - public AzureKeyVaultClientDelegate(KeyVaultClient keyVaultClient) { + AzureKeyVaultClientDelegate(KeyVaultClient keyVaultClient) { this.keyVaultClient = Objects.requireNonNull(keyVaultClient); } - public SecretBundle getSecret(String vaultBaseUrl, String secretName) { + SecretBundle getSecret(String vaultBaseUrl, String secretName) { return keyVaultClient.getSecret(vaultBaseUrl, secretName); } - public SecretBundle setSecret(SetSecretRequest setSecretRequest) { + SecretBundle setSecret(SetSecretRequest setSecretRequest) { return keyVaultClient.setSecret(setSecretRequest); } } diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientFactory.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientFactory.java index 53960a2865..885261d42b 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientFactory.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultClientFactory.java @@ -3,15 +3,15 @@ import com.microsoft.azure.keyvault.KeyVaultClient; import com.microsoft.rest.credentials.ServiceClientCredentials; -public class AzureKeyVaultClientFactory { +class AzureKeyVaultClientFactory { private final ServiceClientCredentials clientCredentials; - public AzureKeyVaultClientFactory(ServiceClientCredentials clientCredentials) { + AzureKeyVaultClientFactory(ServiceClientCredentials clientCredentials) { this.clientCredentials = clientCredentials; } - public KeyVaultClient getAuthenticatedClient() { + KeyVaultClient getAuthenticatedClient() { return new KeyVaultClient(clientCredentials); } } diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultService.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultService.java index f0322589d0..0698cda7ad 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultService.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultService.java @@ -3,6 +3,11 @@ import com.microsoft.azure.keyvault.models.SecretBundle; import com.microsoft.azure.keyvault.requests.SetSecretRequest; import com.quorum.tessera.config.AzureKeyVaultConfig; +import com.quorum.tessera.config.vault.data.AzureGetSecretData; +import com.quorum.tessera.config.vault.data.AzureSetSecretData; +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; +import com.quorum.tessera.key.vault.KeyVaultException; import com.quorum.tessera.key.vault.KeyVaultService; import com.quorum.tessera.key.vault.VaultSecretNotFoundException; @@ -12,7 +17,7 @@ public class AzureKeyVaultService implements KeyVaultService { private String vaultUrl; private AzureKeyVaultClientDelegate azureKeyVaultClientDelegate; - public AzureKeyVaultService(AzureKeyVaultConfig keyVaultConfig, AzureKeyVaultClientDelegate azureKeyVaultClientDelegate) { + AzureKeyVaultService(AzureKeyVaultConfig keyVaultConfig, AzureKeyVaultClientDelegate azureKeyVaultClientDelegate) { if(Objects.nonNull(keyVaultConfig)) { this.vaultUrl = keyVaultConfig.getUrl(); } @@ -20,19 +25,32 @@ public AzureKeyVaultService(AzureKeyVaultConfig keyVaultConfig, AzureKeyVaultCli this.azureKeyVaultClientDelegate = azureKeyVaultClientDelegate; } - public String getSecret(String secretName) { - SecretBundle secretBundle = azureKeyVaultClientDelegate.getSecret(vaultUrl, secretName); + @Override + public String getSecret(GetSecretData getSecretData) { + if(!(getSecretData instanceof AzureGetSecretData)) { + throw new KeyVaultException("Incorrect data type passed to AzureKeyVaultService. Type was " + getSecretData.getType()); + } + + AzureGetSecretData azureGetSecretData = (AzureGetSecretData) getSecretData; + + SecretBundle secretBundle = azureKeyVaultClientDelegate.getSecret(vaultUrl, azureGetSecretData.getSecretName()); if(secretBundle == null) { - throw new VaultSecretNotFoundException("Azure Key Vault secret " + secretName + " was not found in vault " + vaultUrl); + throw new VaultSecretNotFoundException("Azure Key Vault secret " + azureGetSecretData.getSecretName() + " was not found in vault " + vaultUrl); } return secretBundle.value(); } @Override - public SecretBundle setSecret(String secretName, String secret) { - SetSecretRequest setSecretRequest = new SetSecretRequest.Builder(vaultUrl, secretName, secret).build(); + public Object setSecret(SetSecretData setSecretData) { + if(!(setSecretData instanceof AzureSetSecretData)) { + throw new KeyVaultException("Incorrect data type passed to AzureKeyVaultService. Type was " + setSecretData.getType()); + } + + AzureSetSecretData azureSetSecretData = (AzureSetSecretData) setSecretData; + + SetSecretRequest setSecretRequest = new SetSecretRequest.Builder(vaultUrl, azureSetSecretData.getSecretName(), azureSetSecretData.getSecret()).build(); return this.azureKeyVaultClientDelegate.setSecret(setSecretRequest); } diff --git a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactory.java b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactory.java index c5ceb14d1d..82722e8e41 100644 --- a/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactory.java +++ b/key-vault/azure-key-vault/src/main/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactory.java @@ -11,8 +11,8 @@ public class AzureKeyVaultServiceFactory implements KeyVaultServiceFactory { - private final String clientIdEnvVar = "AZURE_CLIENT_ID"; - private final String clientSecretEnvVar = "AZURE_CLIENT_SECRET"; + private static final String clientIdEnvVar = "AZURE_CLIENT_ID"; + private static final String clientSecretEnvVar = "AZURE_CLIENT_SECRET"; @Override public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { @@ -28,7 +28,7 @@ public KeyVaultService create(Config config, EnvironmentVariableProvider envProv AzureKeyVaultConfig keyVaultConfig = Optional.ofNullable(config.getKeys()) .map(KeyConfiguration::getAzureKeyVaultConfig) - .orElseThrow(() -> new ConfigException(new RuntimeException("Trying to create Azure key vault but no Azure configuration provided in the configfile"))); + .orElseThrow(() -> new ConfigException(new RuntimeException("Trying to create Azure key vault connection but no Azure configuration provided"))); return new AzureKeyVaultService( keyVaultConfig, diff --git a/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactoryTest.java b/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactoryTest.java index 9e1da39841..6f9f4da2cb 100644 --- a/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactoryTest.java +++ b/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceFactoryTest.java @@ -78,7 +78,7 @@ public void nullKeyConfigurationThrowsException() { Throwable ex = catchThrowable(() -> azureKeyVaultServiceFactory.create(config, envProvider)); assertThat(ex).isExactlyInstanceOf(ConfigException.class); - assertThat(ex.getMessage()).contains("Trying to create Azure key vault but no Azure configuration provided in the configfile"); + assertThat(ex.getMessage()).contains("Trying to create Azure key vault connection but no Azure configuration provided"); } @Test @@ -91,7 +91,7 @@ public void nullKeyVaultConfigurationThrowsException() { Throwable ex = catchThrowable(() -> azureKeyVaultServiceFactory.create(config, envProvider)); assertThat(ex).isExactlyInstanceOf(ConfigException.class); - assertThat(ex.getMessage()).contains("Trying to create Azure key vault but no Azure configuration provided in the configfile"); + assertThat(ex.getMessage()).contains("Trying to create Azure key vault connection but no Azure configuration provided"); } @Test diff --git a/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceTest.java b/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceTest.java index 0d32628839..d38b9959b9 100644 --- a/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceTest.java +++ b/key-vault/azure-key-vault/src/test/java/com/quorum/tessera/key/vault/azure/AzureKeyVaultServiceTest.java @@ -3,6 +3,11 @@ import com.microsoft.azure.keyvault.models.SecretBundle; import com.microsoft.azure.keyvault.requests.SetSecretRequest; import com.quorum.tessera.config.AzureKeyVaultConfig; +import com.quorum.tessera.config.vault.data.AzureGetSecretData; +import com.quorum.tessera.config.vault.data.AzureSetSecretData; +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; +import com.quorum.tessera.key.vault.KeyVaultException; import com.quorum.tessera.key.vault.VaultSecretNotFoundException; import org.junit.Before; import org.junit.Test; @@ -21,7 +26,7 @@ public void setUp() { } @Test - public void exceptionThrownIfKeyNotFoundInVault() { + public void getSecretExceptionThrownIfKeyNotFoundInVault() { String secretName = "secret"; String vaultUrl = "vaultUrl"; @@ -31,7 +36,10 @@ public void exceptionThrownIfKeyNotFoundInVault() { AzureKeyVaultService azureKeyVaultService = new AzureKeyVaultService(keyVaultConfig, azureKeyVaultClientDelegate); - Throwable throwable = catchThrowable(() -> azureKeyVaultService.getSecret(secretName)); + AzureGetSecretData getSecretData = mock(AzureGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + + Throwable throwable = catchThrowable(() -> azureKeyVaultService.getSecret(getSecretData)); assertThat(throwable).isInstanceOf(VaultSecretNotFoundException.class); assertThat(throwable).hasMessageContaining("Azure Key Vault secret " + secretName + " was not found in vault " + vaultUrl); @@ -47,7 +55,11 @@ public void getSecretUsingUrlInConfig() { when(azureKeyVaultClientDelegate.getSecret(url, secretId)).thenReturn(new SecretBundle()); AzureKeyVaultService azureKeyVaultService = new AzureKeyVaultService(keyVaultConfig, azureKeyVaultClientDelegate); - azureKeyVaultService.getSecret(secretId); + + AzureGetSecretData getSecretData = mock(AzureGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretId); + + azureKeyVaultService.getSecret(getSecretData); verify(azureKeyVaultClientDelegate).getSecret(url, secretId); } @@ -58,11 +70,27 @@ public void vaultUrlIsNotSetIfKeyVaultConfigNotDefined() { AzureKeyVaultService azureKeyVaultService = new AzureKeyVaultService(null, azureKeyVaultClientDelegate); - azureKeyVaultService.getSecret("secret"); + AzureGetSecretData getSecretData = mock(AzureGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn("secret"); + + + azureKeyVaultService.getSecret(getSecretData); verify(azureKeyVaultClientDelegate).getSecret(null, "secret"); } + @Test + public void getSecretThrowsExceptionIfWrongDataImplProvided() { + AzureKeyVaultService azureKeyVaultService = new AzureKeyVaultService(null, azureKeyVaultClientDelegate); + + GetSecretData wrongImpl = mock(GetSecretData.class); + + Throwable ex = catchThrowable(() -> azureKeyVaultService.getSecret(wrongImpl)); + + assertThat(ex).isInstanceOf(KeyVaultException.class); + assertThat(ex.getMessage()).isEqualTo("Incorrect data type passed to AzureKeyVaultService. Type was null"); + } + @Test public void setSecretRequestIsUsedToRetrieveSecretFromVault() { AzureKeyVaultConfig keyVaultConfig = new AzureKeyVaultConfig("url"); @@ -72,7 +100,11 @@ public void setSecretRequestIsUsedToRetrieveSecretFromVault() { String secretName = "id"; String secret = "secret"; - azureKeyVaultService.setSecret(secretName, secret); + AzureSetSecretData setSecretData = mock(AzureSetSecretData.class); + when(setSecretData.getSecretName()).thenReturn(secretName); + when(setSecretData.getSecret()).thenReturn(secret); + + azureKeyVaultService.setSecret(setSecretData); SetSecretRequest expected = new SetSecretRequest.Builder(keyVaultConfig.getUrl(), secretName, secret).build(); @@ -81,4 +113,16 @@ public void setSecretRequestIsUsedToRetrieveSecretFromVault() { assertThat(argument.getValue()).isEqualToComparingFieldByField(expected); } + + @Test + public void setSecretThrowsExceptionIfWrongDataImplProvided() { + AzureKeyVaultService azureKeyVaultService = new AzureKeyVaultService(null, azureKeyVaultClientDelegate); + + SetSecretData wrongImpl = mock(SetSecretData.class); + + Throwable ex = catchThrowable(() -> azureKeyVaultService.setSecret(wrongImpl)); + + assertThat(ex).isInstanceOf(KeyVaultException.class); + assertThat(ex.getMessage()).isEqualTo("Incorrect data type passed to AzureKeyVaultService. Type was null"); + } } diff --git a/key-vault/hashicorp-key-vault/pom.xml b/key-vault/hashicorp-key-vault/pom.xml new file mode 100644 index 0000000000..41a2ab782c --- /dev/null +++ b/key-vault/hashicorp-key-vault/pom.xml @@ -0,0 +1,33 @@ + + + + key-vault + com.quorum.tessera + 0.8-SNAPSHOT + + 4.0.0 + + hashicorp-key-vault + + + + + com.quorum.tessera + key-vault-api + + + + org.springframework.vault + spring-vault-core + + + + com.squareup.okhttp3 + okhttp + + + + + \ No newline at end of file diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetException.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetException.java new file mode 100644 index 0000000000..f096b19abb --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetException.java @@ -0,0 +1,9 @@ +package com.quorum.tessera.key.vault.hashicorp; + +class HashicorpCredentialNotSetException extends IllegalStateException { + + HashicorpCredentialNotSetException(String message) { + super(message); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultService.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultService.java new file mode 100644 index 0000000000..ac84639feb --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultService.java @@ -0,0 +1,57 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; +import com.quorum.tessera.key.vault.KeyVaultException; +import com.quorum.tessera.key.vault.KeyVaultService; +import org.springframework.vault.support.Versioned; + +import java.util.Map; + +public class HashicorpKeyVaultService implements KeyVaultService { + + private final KeyValueOperationsDelegateFactory keyValueOperationsDelegateFactory; + + HashicorpKeyVaultService(KeyValueOperationsDelegateFactory keyValueOperationsDelegateFactory) { + this.keyValueOperationsDelegateFactory = keyValueOperationsDelegateFactory; + } + + + @Override + public String getSecret(GetSecretData getSecretData) { + if(!(getSecretData instanceof HashicorpGetSecretData)) { + throw new KeyVaultException("Incorrect data type passed to HashicorpKeyVaultService. Type was " + getSecretData.getType()); + } + + HashicorpGetSecretData hashicorpGetSecretData = (HashicorpGetSecretData) getSecretData; + + KeyValueOperationsDelegate keyValueOperationsDelegate = keyValueOperationsDelegateFactory.create(hashicorpGetSecretData.getSecretEngineName()); + + Versioned> versionedResponse = keyValueOperationsDelegate.get(hashicorpGetSecretData); + + if(versionedResponse == null || !versionedResponse.hasData()) { + throw new HashicorpVaultException("No data found at " + hashicorpGetSecretData.getSecretEngineName() + "/" + hashicorpGetSecretData.getSecretName()); + } + + if(!versionedResponse.getData().containsKey(hashicorpGetSecretData.getValueId())) { + throw new HashicorpVaultException("No value with id " + hashicorpGetSecretData.getValueId() + " found at " + hashicorpGetSecretData.getSecretEngineName() + "/" + hashicorpGetSecretData.getSecretName()); + } + + return versionedResponse.getData().get(hashicorpGetSecretData.getValueId()).toString(); + } + + @Override + public Object setSecret(SetSecretData setSecretData) { + if(!(setSecretData instanceof HashicorpSetSecretData)) { + throw new KeyVaultException("Incorrect data type passed to HashicorpKeyVaultService. Type was " + setSecretData.getType()); + } + + HashicorpSetSecretData hashicorpSetSecretData = (HashicorpSetSecretData) setSecretData; + + KeyValueOperationsDelegate keyValueOperationsDelegate = keyValueOperationsDelegateFactory.create(hashicorpSetSecretData.getSecretEngineName()); + + return keyValueOperationsDelegate.set(hashicorpSetSecretData); + } +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactory.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactory.java new file mode 100644 index 0000000000..cdfec98703 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactory.java @@ -0,0 +1,91 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.Optional; + +public class HashicorpKeyVaultServiceFactory implements KeyVaultServiceFactory { + + private static final String roleIdEnvVar = "HASHICORP_ROLE_ID"; + private static final String secretIdEnvVar = "HASHICORP_SECRET_ID"; + private static final String authTokenEnvVar = "HASHICORP_TOKEN"; + + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + Objects.requireNonNull(config); + Objects.requireNonNull(envProvider); + + HashicorpKeyVaultServiceFactoryUtil util = new HashicorpKeyVaultServiceFactoryUtil(roleIdEnvVar, secretIdEnvVar, authTokenEnvVar); + + return this.create(config, envProvider, util); + } + + //This method should not be called directly. It has been left package-private to enable injection of util during testing + KeyVaultService create(Config config, EnvironmentVariableProvider envProvider, HashicorpKeyVaultServiceFactoryUtil util) { + Objects.requireNonNull(config); + Objects.requireNonNull(envProvider); + Objects.requireNonNull(util); + + final String roleId = envProvider.getEnv(roleIdEnvVar); + final String secretId = envProvider.getEnv(secretIdEnvVar); + final String authToken = envProvider.getEnv(authTokenEnvVar); + + if(roleId == null && secretId == null && authToken == null) { + throw new HashicorpCredentialNotSetException("Environment variables must be set to authenticate with Hashicorp Vault. Set the " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables if using the AppRole authentication method. Set the " + authTokenEnvVar + " environment variable if using another authentication method."); + } + else if(isOnlyOneInputNull(roleId, secretId)) { + throw new HashicorpCredentialNotSetException("Only one of the " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables to authenticate with Hashicorp Vault using the AppRole method has been set"); + } + + HashicorpKeyVaultConfig keyVaultConfig = Optional.ofNullable(config.getKeys()) + .map(KeyConfiguration::getHashicorpKeyVaultConfig) + .orElseThrow(() -> new ConfigException(new RuntimeException("Trying to create Hashicorp Vault connection but no Vault configuration provided"))); + + VaultEndpoint vaultEndpoint; + + try { + vaultEndpoint = VaultEndpoint.from(new URI(keyVaultConfig.getUrl())); + } catch (URISyntaxException | IllegalArgumentException e) { + throw new ConfigException(new RuntimeException("Provided Hashicorp Vault url is incorrectly formatted", e)); + } + + SslConfiguration sslConfiguration = util.configureSsl(keyVaultConfig, envProvider); + + ClientOptions clientOptions = new ClientOptions(); + + ClientHttpRequestFactory clientHttpRequestFactory = util.createClientHttpRequestFactory(clientOptions, sslConfiguration); + + ClientAuthentication clientAuthentication = util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint); + + SessionManager sessionManager = new SimpleSessionManager(clientAuthentication); + VaultOperations vaultOperations = new VaultTemplate(vaultEndpoint, clientHttpRequestFactory, sessionManager); + + return new HashicorpKeyVaultService( + new KeyValueOperationsDelegateFactory(vaultOperations) + ); + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.HASHICORP; + } + + private boolean isOnlyOneInputNull(Object obj1, Object obj2) { + return Objects.isNull(obj1) ^ Objects.isNull(obj2); + } +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtil.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtil.java new file mode 100644 index 0000000000..5031465f14 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtil.java @@ -0,0 +1,97 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.HashicorpKeyVaultConfig; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.authentication.AppRoleAuthentication; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.config.ClientHttpRequestFactoryFactory; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.web.client.RestOperations; + +import java.util.Objects; + +class HashicorpKeyVaultServiceFactoryUtil { + + private final String roleIdEnvVar; + private final String secretIdEnvVar; + private final String authTokenEnvVar; + private static final String keyStorePwdEnvVar = "HASHICORP_CLIENT_KEYSTORE_PWD"; + private static final String trustStorePwdEnvVar = "HASHICORP_CLIENT_TRUSTSTORE_PWD"; + + HashicorpKeyVaultServiceFactoryUtil(String roleIdEnvVar, String secretIdEnvVar, String authTokenEnvVar) { + this.roleIdEnvVar = roleIdEnvVar; + this.secretIdEnvVar = secretIdEnvVar; + this.authTokenEnvVar = authTokenEnvVar; + } + + SslConfiguration configureSsl(HashicorpKeyVaultConfig keyVaultConfig, EnvironmentVariableProvider envProvider) { + if(keyVaultConfig.getTlsKeyStorePath() != null && keyVaultConfig.getTlsTrustStorePath() != null) { + + Resource clientKeyStore = new FileSystemResource(keyVaultConfig.getTlsKeyStorePath().toFile()); + Resource clientTrustStore = new FileSystemResource(keyVaultConfig.getTlsTrustStorePath().toFile()); + + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = SslConfiguration.KeyStoreConfiguration.of( + clientKeyStore, + envProvider.getEnvAsCharArray(keyStorePwdEnvVar) + ); + + SslConfiguration.KeyStoreConfiguration trustStoreConfiguration = SslConfiguration.KeyStoreConfiguration.of( + clientTrustStore, + envProvider.getEnvAsCharArray(trustStorePwdEnvVar) + ); + + return new SslConfiguration(keyStoreConfiguration, trustStoreConfiguration); + + } else if (keyVaultConfig.getTlsTrustStorePath() != null) { + + Resource clientTrustStore = new FileSystemResource(keyVaultConfig.getTlsTrustStorePath().toFile()); + + return SslConfiguration.forTrustStore(clientTrustStore, envProvider.getEnvAsCharArray(trustStorePwdEnvVar)); + + } else { + return SslConfiguration.unconfigured(); + } + } + + ClientHttpRequestFactory createClientHttpRequestFactory(ClientOptions clientOptions, SslConfiguration sslConfiguration) { + return ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration); + } + + ClientAuthentication configureClientAuthentication(HashicorpKeyVaultConfig keyVaultConfig, EnvironmentVariableProvider envProvider, ClientHttpRequestFactory clientHttpRequestFactory, VaultEndpoint vaultEndpoint) { + + final String roleId = envProvider.getEnv(roleIdEnvVar); + final String secretId = envProvider.getEnv(secretIdEnvVar); + final String authToken = envProvider.getEnv(authTokenEnvVar); + + if(roleId != null && secretId != null) { + + AppRoleAuthenticationOptions appRoleAuthenticationOptions = AppRoleAuthenticationOptions.builder() + .path(keyVaultConfig.getApprolePath()) + .roleId(AppRoleAuthenticationOptions.RoleId.provided(roleId)) + .secretId(AppRoleAuthenticationOptions.SecretId.provided(secretId)) + .build(); + + RestOperations restOperations = VaultClients.createRestTemplate(vaultEndpoint, clientHttpRequestFactory); + + return new AppRoleAuthentication(appRoleAuthenticationOptions, restOperations); + + } else if (Objects.isNull(roleId) != Objects.isNull(secretId)) { + + throw new HashicorpCredentialNotSetException("Both " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables must be set to use the AppRole authentication method"); + + } else if (authToken == null){ + + throw new HashicorpCredentialNotSetException("Both " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables must be set to use the AppRole authentication method. Alternatively set " + authTokenEnvVar + " to authenticate using the Token method"); + } + + return new TokenAuthentication(authToken); + } +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultException.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultException.java new file mode 100644 index 0000000000..115a3deeca --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultException.java @@ -0,0 +1,14 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.key.vault.KeyVaultException; + +class HashicorpVaultException extends KeyVaultException { + + HashicorpVaultException(Throwable cause) { + super(cause); + } + + HashicorpVaultException(String message) { + super(message); + } +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegate.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegate.java new file mode 100644 index 0000000000..841a6e48fd --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegate.java @@ -0,0 +1,27 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import org.springframework.vault.core.VaultVersionedKeyValueOperations; +import org.springframework.vault.support.Versioned; + +import java.util.Map; + +class KeyValueOperationsDelegate { + + private final VaultVersionedKeyValueOperations keyValueOperations; + + KeyValueOperationsDelegate(VaultVersionedKeyValueOperations keyValueOperations) { + this.keyValueOperations = keyValueOperations; + } + + Versioned> get(HashicorpGetSecretData getSecretData) { + //if version 0 then latest version retrieved + return keyValueOperations.get(getSecretData.getSecretName(), Versioned.Version.from(getSecretData.getSecretVersion())); + } + + Versioned.Metadata set(HashicorpSetSecretData setSecretData) { + return keyValueOperations.put(setSecretData.getSecretName(), setSecretData.getNameValuePairs()); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateFactory.java b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateFactory.java new file mode 100644 index 0000000000..4155cb0f7b --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateFactory.java @@ -0,0 +1,21 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.core.VaultVersionedKeyValueOperations; +import org.springframework.vault.core.VaultVersionedKeyValueTemplate; + +class KeyValueOperationsDelegateFactory { + + private final VaultOperations vaultOperations; + + KeyValueOperationsDelegateFactory(VaultOperations vaultOperations) { + this.vaultOperations = vaultOperations; + } + + KeyValueOperationsDelegate create(String secretEngineName) { + VaultVersionedKeyValueOperations keyValueOperations = new VaultVersionedKeyValueTemplate(vaultOperations, secretEngineName); + + return new KeyValueOperationsDelegate(keyValueOperations); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/key-vault/hashicorp-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory new file mode 100644 index 0000000000..dfa2a119c7 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -0,0 +1 @@ +com.quorum.tessera.key.vault.hashicorp.HashicorpKeyVaultServiceFactory \ No newline at end of file diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetExceptionTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetExceptionTest.java new file mode 100644 index 0000000000..2ff2239b51 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpCredentialNotSetExceptionTest.java @@ -0,0 +1,17 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpCredentialNotSetExceptionTest { + + @Test + public void createWithMessage() { + final String msg = "msg"; + HashicorpCredentialNotSetException exception = new HashicorpCredentialNotSetException(msg); + + assertThat(exception).hasMessage(msg); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryTest.java new file mode 100644 index 0000000000..ca6ef29e1e --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryTest.java @@ -0,0 +1,292 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.key.vault.KeyVaultService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class HashicorpKeyVaultServiceFactoryTest { + + private HashicorpKeyVaultServiceFactory keyVaultServiceFactory; + + private Config config; + + private EnvironmentVariableProvider envProvider; + + private HashicorpKeyVaultServiceFactoryUtil keyVaultServiceFactoryUtil; + + private String noCredentialsExceptionMsg = "Environment variables must be set to authenticate with Hashicorp Vault. Set the HASHICORP_ROLE_ID and HASHICORP_SECRET_ID environment variables if using the AppRole authentication method. Set the HASHICORP_TOKEN environment variable if using another authentication method."; + + private String approleCredentialsExceptionMsg = "Only one of the HASHICORP_ROLE_ID and HASHICORP_SECRET_ID environment variables to authenticate with Hashicorp Vault using the AppRole method has been set"; + + @Before + public void setUp() { + this.keyVaultServiceFactory = new HashicorpKeyVaultServiceFactory(); + this.config = mock(Config.class); + this.envProvider = mock(EnvironmentVariableProvider.class); + this.keyVaultServiceFactoryUtil = mock(HashicorpKeyVaultServiceFactoryUtil.class); + } + + @Test(expected = NullPointerException.class) + public void nullConfigThrowsException() { + keyVaultServiceFactory.create(null, envProvider); + } + + @Test(expected = NullPointerException.class) + public void nullEnvVarProviderThrowsException() { + keyVaultServiceFactory.create(config, null); + } + + @Test + public void getType() { + assertThat(keyVaultServiceFactory.getType()).isEqualTo(KeyVaultType.HASHICORP); + } + + @Test + public void exceptionThrownIfNoAuthEnvVarsSet() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex).hasMessage(noCredentialsExceptionMsg); + } + + @Test + public void exceptionThrownIfOnlyRoleIdAuthEnvVarSet() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex).hasMessage(approleCredentialsExceptionMsg); + } + + @Test + public void exceptionThrownIfOnlySecretIdAuthEnvVarSet() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex).hasMessage(approleCredentialsExceptionMsg); + } + + @Test + public void exceptionThrownIfOnlyRoleIdAndTokenAuthEnvVarsSet() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex).hasMessage(approleCredentialsExceptionMsg); + } + + @Test + public void exceptionThrownIfOnlySecretIdAndTokenAuthEnvVarsSet() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex).hasMessage(approleCredentialsExceptionMsg); + } + + @Test + public void roleIdAndSecretIdAuthEnvVarsAreSetIsAllowed() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + //Exception unrelated to env vars will be thrown + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isNotInstanceOf(HashicorpCredentialNotSetException.class); + } + + @Test + public void onlyTokenAuthEnvVarIsSetIsAllowed() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn(null); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + //Exception unrelated to env vars will be thrown + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isNotInstanceOf(HashicorpCredentialNotSetException.class); + } + + @Test + public void allAuthEnvVarsSetIsAllowed() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + //Exception unrelated to env vars will be thrown + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isNotInstanceOf(HashicorpCredentialNotSetException.class); + } + + @Test + public void exceptionThrownIfProvidedConfigHasNoKeyConfiguration() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + when(config.getKeys()).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(ConfigException.class); + assertThat(ex).hasMessageContaining("Trying to create Hashicorp Vault connection but no Vault configuration provided"); + } + + @Test + public void exceptionThrownIfProvidedConfigHasNoHashicorpKeyVaultConfig() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn(null); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(config.getKeys()).thenReturn(keyConfiguration); + + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(ConfigException.class); + assertThat(ex).hasMessageContaining("Trying to create Hashicorp Vault connection but no Vault configuration provided"); + } + + @Test + public void exceptionThrownIfKeyVaultConfigUrlSyntaxIncorrect() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(config.getKeys()).thenReturn(keyConfiguration); + + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + when(keyVaultConfig.getUrl()).thenReturn("noschemeurl"); + when(keyVaultConfig.getApprolePath()).thenReturn("approle"); + + setUpUtilMocks(keyVaultConfig); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider, keyVaultServiceFactoryUtil)); + + assertThat(ex).isExactlyInstanceOf(ConfigException.class); + assertThat(ex.getMessage()).contains("Provided Hashicorp Vault url is incorrectly formatted"); + } + + @Test + public void exceptionThrownIfKeyVaultConfigUrlIsMalformed() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(config.getKeys()).thenReturn(keyConfiguration); + + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + when(keyVaultConfig.getUrl()).thenReturn("http://malformedurl:-1"); + when(keyVaultConfig.getApprolePath()).thenReturn("approle"); + + setUpUtilMocks(keyVaultConfig); + + Throwable ex = catchThrowable(() -> keyVaultServiceFactory.create(config, envProvider, keyVaultServiceFactoryUtil)); + + assertThat(ex).isExactlyInstanceOf(ConfigException.class); + assertThat(ex.getMessage()).contains("Provided Hashicorp Vault url is incorrectly formatted"); + } + + private void setUpUtilMocks(HashicorpKeyVaultConfig keyVaultConfig) { + SslConfiguration sslConfiguration = mock(SslConfiguration.class); + when(keyVaultServiceFactoryUtil.configureSsl(keyVaultConfig, envProvider)).thenReturn(sslConfiguration); + + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + when(keyVaultServiceFactoryUtil.createClientHttpRequestFactory( + any(ClientOptions.class), + eq(sslConfiguration)) + ).thenReturn(clientHttpRequestFactory); + + ClientAuthentication clientAuthentication = mock(ClientAuthentication.class); + when(keyVaultServiceFactoryUtil.configureClientAuthentication( + eq(keyVaultConfig), + eq(envProvider), + eq(clientHttpRequestFactory), + any(VaultEndpoint.class)) + ).thenReturn(clientAuthentication); + } + + @Test + public void returnedValueIsCorrectType() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(config.getKeys()).thenReturn(keyConfiguration); + + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + when(keyVaultConfig.getUrl()).thenReturn("http://someurl"); + when(keyVaultConfig.getApprolePath()).thenReturn("approle"); + + setUpUtilMocks(keyVaultConfig); + + KeyVaultService result = keyVaultServiceFactory.create(config, envProvider, keyVaultServiceFactoryUtil); + + assertThat(result).isInstanceOf(HashicorpKeyVaultService.class); + } + + @Test + public void returnedValueIsCorrectTypeUsing2ArgConstructor() { + when(envProvider.getEnv("HASHICORP_ROLE_ID")).thenReturn("role-id"); + when(envProvider.getEnv("HASHICORP_SECRET_ID")).thenReturn("secret-id"); + when(envProvider.getEnv("HASHICORP_TOKEN")).thenReturn("token"); + + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(config.getKeys()).thenReturn(keyConfiguration); + + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + when(keyConfiguration.getHashicorpKeyVaultConfig()).thenReturn(keyVaultConfig); + + when(keyVaultConfig.getUrl()).thenReturn("http://someurl"); + when(keyVaultConfig.getApprolePath()).thenReturn("approle"); + + setUpUtilMocks(keyVaultConfig); + + KeyVaultService result = keyVaultServiceFactory.create(config, envProvider); + + assertThat(result).isInstanceOf(HashicorpKeyVaultService.class); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtilTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtilTest.java new file mode 100644 index 0000000000..c654be2942 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceFactoryUtilTest.java @@ -0,0 +1,208 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.HashicorpKeyVaultConfig; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.vault.authentication.AppRoleAuthentication; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class HashicorpKeyVaultServiceFactoryUtilTest { + + private HashicorpKeyVaultServiceFactoryUtil util; + + private final String roleIdEnvVar = "HASHICORP_ROLE_ID"; + private final String secretIdEnvVar = "HASHICORP_SECRET_ID"; + private final String authTokenEnvVar = "HASHICORP_TOKEN"; + + @Before + public void setUp() { + this.util = new HashicorpKeyVaultServiceFactoryUtil(roleIdEnvVar, secretIdEnvVar, authTokenEnvVar); + } + + @Test + public void configureSslUsesKeyStoreAndTrustStoreIfBothProvided() throws Exception { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + + Path path = Files.createTempFile(UUID.randomUUID().toString(), ".tmp"); + path.toFile().deleteOnExit(); + + when(keyVaultConfig.getTlsKeyStorePath()).thenReturn(path); + when(keyVaultConfig.getTlsTrustStorePath()).thenReturn(path); + + SslConfiguration result = util.configureSsl(keyVaultConfig, envProvider); + + assertThat(result.getKeyStoreConfiguration().isPresent()).isTrue(); + assertThat(result.getTrustStoreConfiguration().isPresent()).isTrue(); + } + + @Test + public void configureSslUsesTrustStoreOnlyIfProvided() throws Exception { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + + Path path = Files.createTempFile(UUID.randomUUID().toString(), ".tmp"); + path.toFile().deleteOnExit(); + + when(keyVaultConfig.getTlsKeyStorePath()).thenReturn(null); + when(keyVaultConfig.getTlsTrustStorePath()).thenReturn(path); + + SslConfiguration result = util.configureSsl(keyVaultConfig, envProvider); + + assertThat(result.getKeyStoreConfiguration().isPresent()).isFalse(); + assertThat(result.getTrustStoreConfiguration().isPresent()).isTrue(); + } + + @Test + public void configureSslUsesNoKeyStoresIfNoneProvided() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + + when(keyVaultConfig.getTlsKeyStorePath()).thenReturn(null); + when(keyVaultConfig.getTlsTrustStorePath()).thenReturn(null); + + SslConfiguration result = util.configureSsl(keyVaultConfig, envProvider); + + assertThat(result.getKeyStoreConfiguration().isPresent()).isFalse(); + assertThat(result.getTrustStoreConfiguration().isPresent()).isFalse(); + } + + @Test + public void createClientHttpRequestFactory() { + ClientOptions clientOptions = mock(ClientOptions.class); + SslConfiguration sslConfiguration = mock(SslConfiguration.class); + + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = mock(SslConfiguration.KeyStoreConfiguration.class); + when(sslConfiguration.getKeyStoreConfiguration()).thenReturn(keyStoreConfiguration); + when(sslConfiguration.getTrustStoreConfiguration()).thenReturn(keyStoreConfiguration); + + when(clientOptions.getConnectionTimeout()).thenReturn(Duration.ZERO); + when(clientOptions.getReadTimeout()).thenReturn(Duration.ZERO); + + ClientHttpRequestFactory result = util.createClientHttpRequestFactory(clientOptions, sslConfiguration); + + assertThat(result).isInstanceOf(OkHttp3ClientHttpRequestFactory.class); + } + + @Test + public void configureClientAuthenticationIfAllEnvVarsSetThenAppRoleMethod() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn("role-id"); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn("secret-id"); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn("token"); + + when(keyVaultConfig.getApprolePath()).thenReturn("approle"); + + ClientAuthentication result = util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint); + + assertThat(result).isInstanceOf(AppRoleAuthentication.class); + } + + @Test + public void configureClientAuthenticationIfOnlyRoleIdAndSecretIdSetThenAppRoleMethod() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn("role-id"); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn("secret-id"); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn(null); + + when(keyVaultConfig.getApprolePath()).thenReturn("somepath"); + + ClientAuthentication result = util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint); + + assertThat(result).isInstanceOf(AppRoleAuthentication.class); + } + + + @Test + public void configureClientAuthenticationIfOnlyRoleIdSetThenException() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn("role-id"); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn(null); + + Throwable ex = catchThrowable(() -> util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint)); + + assertThat(ex).isExactlyInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex.getMessage()).isEqualTo("Both " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables must be set to use the AppRole authentication method"); + } + + @Test + public void configureClientAuthenticationIfOnlySecretIdSetThenException() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn("secret-id"); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn(null); + + Throwable ex = catchThrowable(() -> util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint)); + + assertThat(ex).isExactlyInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex.getMessage()).isEqualTo("Both " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables must be set to use the AppRole authentication method"); + } + + @Test + public void configureClientAuthenticationIfOnlyTokenSetThenTokenMethod() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn("token"); + + ClientAuthentication result = util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint); + + assertThat(result).isInstanceOf(TokenAuthentication.class); + } + + @Test + public void configureClientAuthenticationIfNoEnvVarSetThenException() { + HashicorpKeyVaultConfig keyVaultConfig = mock(HashicorpKeyVaultConfig.class); + EnvironmentVariableProvider envProvider = mock(EnvironmentVariableProvider.class); + ClientHttpRequestFactory clientHttpRequestFactory = mock(ClientHttpRequestFactory.class); + VaultEndpoint vaultEndpoint = mock(VaultEndpoint.class); + + when(envProvider.getEnv(roleIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(secretIdEnvVar)).thenReturn(null); + when(envProvider.getEnv(authTokenEnvVar)).thenReturn(null); + + Throwable ex = catchThrowable(() -> util.configureClientAuthentication(keyVaultConfig, envProvider, clientHttpRequestFactory, vaultEndpoint)); + + assertThat(ex).isExactlyInstanceOf(HashicorpCredentialNotSetException.class); + assertThat(ex.getMessage()).isEqualTo("Both " + roleIdEnvVar + " and " + secretIdEnvVar + " environment variables must be set to use the AppRole authentication method. Alternatively set " + authTokenEnvVar + " to authenticate using the Token method"); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceTest.java new file mode 100644 index 0000000000..49fc1c1c6e --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpKeyVaultServiceTest.java @@ -0,0 +1,148 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; +import com.quorum.tessera.key.vault.KeyVaultException; +import org.junit.Before; +import org.junit.Test; +import org.springframework.vault.support.Versioned; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class HashicorpKeyVaultServiceTest { + + private HashicorpKeyVaultService keyVaultService; + + private KeyValueOperationsDelegateFactory delegateFactory; + + private KeyValueOperationsDelegate delegate; + + @Before + public void setUp() { + this.delegateFactory = mock(KeyValueOperationsDelegateFactory.class); + this.delegate = mock(KeyValueOperationsDelegate.class); + when(delegateFactory.create(anyString())).thenReturn(delegate); + + this.keyVaultService = new HashicorpKeyVaultService(delegateFactory); + } + + @Test + public void getSecret() { + HashicorpGetSecretData getSecretData = mock(HashicorpGetSecretData.class); + + when(getSecretData.getSecretEngineName()).thenReturn("secretEngine"); + when(getSecretData.getSecretName()).thenReturn("secretName"); + when(getSecretData.getValueId()).thenReturn("keyId"); + + Versioned versionedResponse = mock(Versioned.class); + + when(delegate.get(any(HashicorpGetSecretData.class))).thenReturn(versionedResponse); + + when(versionedResponse.hasData()).thenReturn(true); + + Map responseData = mock(Map.class); + when(versionedResponse.getData()).thenReturn(responseData); + when(responseData.containsKey("keyId")).thenReturn(true); + String keyValue = "keyvalue"; + when(responseData.get("keyId")).thenReturn(keyValue); + + String result = keyVaultService.getSecret(getSecretData); + + assertThat(result).isEqualTo(keyValue); + } + + @Test + public void getSecretThrowsExceptionIfProvidedDataIsNotCorrectType() { + GetSecretData getSecretData = mock(GetSecretData.class); + when(getSecretData.getType()).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(ex).isExactlyInstanceOf(KeyVaultException.class); + assertThat(ex).hasMessage("Incorrect data type passed to HashicorpKeyVaultService. Type was null"); + } + + @Test + public void getSecretThrowsExceptionIfNullRetrievedFromVault() { + HashicorpGetSecretData getSecretData = new HashicorpGetSecretData("engine", "secretName", "id", 0); + + when(delegate.get(getSecretData)).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(ex).isExactlyInstanceOf(HashicorpVaultException.class); + assertThat(ex).hasMessage("No data found at engine/secretName"); + } + + @Test + public void getSecretThrowsExceptionIfNoDataRetrievedFromVault() { + HashicorpGetSecretData getSecretData = new HashicorpGetSecretData("engine", "secretName", "id", 0); + + Versioned versionedResponse = mock(Versioned.class); + when(versionedResponse.hasData()).thenReturn(false); + + when(delegate.get(getSecretData)).thenReturn(versionedResponse); + + Throwable ex = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(ex).isExactlyInstanceOf(HashicorpVaultException.class); + assertThat(ex).hasMessage("No data found at engine/secretName"); + } + + + @Test + public void getSecretThrowsExceptionIfValueNotFoundForGivenId() { + HashicorpGetSecretData getSecretData = new HashicorpGetSecretData("engine", "secretName", "id", 0); + + Versioned versionedResponse = mock(Versioned.class); + when(versionedResponse.hasData()).thenReturn(true); + + Map responseData = mock(Map.class); + when(versionedResponse.getData()).thenReturn(responseData); + when(responseData.containsKey("id")).thenReturn(false); + + when(delegate.get(getSecretData)).thenReturn(versionedResponse); + + Throwable ex = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(ex).isExactlyInstanceOf(HashicorpVaultException.class); + assertThat(ex).hasMessage("No value with id id found at engine/secretName"); + } + + + @Test + public void setSecretThrowsExceptionIfProvidedDataIsNotCorrectType() { + SetSecretData setSecretData = mock(SetSecretData.class); + when(setSecretData.getType()).thenReturn(null); + + Throwable ex = catchThrowable(() -> keyVaultService.setSecret(setSecretData)); + + assertThat(ex).isExactlyInstanceOf(KeyVaultException.class); + assertThat(ex).hasMessage("Incorrect data type passed to HashicorpKeyVaultService. Type was null"); + } + + + @Test + public void setSecretReturnsMetadataObject() { + HashicorpSetSecretData setSecretData = new HashicorpSetSecretData("engine", "name", Collections.emptyMap()); + + Versioned.Metadata metadata = mock(Versioned.Metadata.class); + when(delegate.set(setSecretData)).thenReturn(metadata); + + Object result = keyVaultService.setSecret(setSecretData); + + assertThat(result).isInstanceOf(Versioned.Metadata.class); + assertThat(result).isEqualTo(metadata); + } + +} diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultExceptionTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultExceptionTest.java new file mode 100644 index 0000000000..1ff410fd42 --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/HashicorpVaultExceptionTest.java @@ -0,0 +1,23 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HashicorpVaultExceptionTest { + @Test + public void createWithMessage() { + final String msg = "msg"; + HashicorpVaultException exception = new HashicorpVaultException(msg); + + assertThat(exception).hasMessage(msg); + } + + @Test + public void createWithCause() { + Throwable cause = new Exception("cause"); + HashicorpVaultException exception = new HashicorpVaultException(cause); + + assertThat(exception).hasCause(cause); + } +} diff --git a/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateTest.java b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateTest.java new file mode 100644 index 0000000000..9aadf8a9fc --- /dev/null +++ b/key-vault/hashicorp-key-vault/src/test/java/com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateTest.java @@ -0,0 +1,64 @@ +package com.quorum.tessera.key.vault.hashicorp; + +import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; +import com.quorum.tessera.config.vault.data.HashicorpSetSecretData; +import org.junit.Before; +import org.junit.Test; +import org.springframework.vault.core.VaultVersionedKeyValueOperations; +import org.springframework.vault.support.Versioned; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class KeyValueOperationsDelegateTest { + + private KeyValueOperationsDelegate delegate; + + private VaultVersionedKeyValueOperations keyValueOperations; + + @Before + public void setUp() { + this.keyValueOperations = mock(VaultVersionedKeyValueOperations.class); + this.delegate = new KeyValueOperationsDelegate(keyValueOperations); + } + + @Test + public void get() { + String secretName = "secretName"; + + HashicorpGetSecretData getSecretData = mock(HashicorpGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + when(getSecretData.getSecretVersion()).thenReturn(0); + + Versioned versionedResponse = mock(Versioned.class); + when(keyValueOperations.get(secretName, Versioned.Version.from(0))).thenReturn(versionedResponse); + + Versioned result = delegate.get(getSecretData); + + verify(keyValueOperations).get(secretName, Versioned.Version.unversioned()); + + assertThat(result).isEqualTo(versionedResponse); + } + + @Test + public void set() { + String secretName = "secretName"; + + HashicorpSetSecretData setSecretData = mock(HashicorpSetSecretData.class); + when(setSecretData.getSecretName()).thenReturn(secretName); + Map nameValuePairs = mock(Map.class); + when(setSecretData.getNameValuePairs()).thenReturn(nameValuePairs); + + Versioned.Metadata metadata = mock(Versioned.Metadata.class); + when(keyValueOperations.put(secretName, nameValuePairs)).thenReturn(metadata); + + Versioned.Metadata result = delegate.set(setSecretData); + + verify(keyValueOperations).put(secretName, nameValuePairs); + + assertThat(result).isEqualTo(metadata); + } + +} diff --git a/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultException.java b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultException.java new file mode 100644 index 0000000000..aa9f4b2a61 --- /dev/null +++ b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultException.java @@ -0,0 +1,13 @@ +package com.quorum.tessera.key.vault; + +public class KeyVaultException extends RuntimeException { + + public KeyVaultException(Throwable cause) { + super(cause); + } + + public KeyVaultException(String message) { + super(message); + } + +} diff --git a/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultService.java b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultService.java index 9db8b313f4..a60015dfbb 100644 --- a/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultService.java +++ b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/KeyVaultService.java @@ -1,8 +1,11 @@ package com.quorum.tessera.key.vault; +import com.quorum.tessera.config.vault.data.GetSecretData; +import com.quorum.tessera.config.vault.data.SetSecretData; + public interface KeyVaultService { - String getSecret(String secretName); + String getSecret(GetSecretData getSecretData); - Object setSecret(String secretName, String secret); + Object setSecret(SetSecretData setSecretData); } diff --git a/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/NoKeyVaultServiceFactoryException.java b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/NoKeyVaultServiceFactoryException.java index b845a4e9fa..7a10d2eaa9 100644 --- a/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/NoKeyVaultServiceFactoryException.java +++ b/key-vault/key-vault-api/src/main/java/com/quorum/tessera/key/vault/NoKeyVaultServiceFactoryException.java @@ -1,8 +1,8 @@ package com.quorum.tessera.key.vault; -public class NoKeyVaultServiceFactoryException extends RuntimeException { +class NoKeyVaultServiceFactoryException extends RuntimeException { - public NoKeyVaultServiceFactoryException(String message) { + NoKeyVaultServiceFactoryException(String message) { super(message); } diff --git a/key-vault/key-vault-api/src/test/java/com/quorum/tessera/key/vault/KeyVaultExceptionTest.java b/key-vault/key-vault-api/src/test/java/com/quorum/tessera/key/vault/KeyVaultExceptionTest.java new file mode 100644 index 0000000000..642ebff3fb --- /dev/null +++ b/key-vault/key-vault-api/src/test/java/com/quorum/tessera/key/vault/KeyVaultExceptionTest.java @@ -0,0 +1,25 @@ +package com.quorum.tessera.key.vault; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KeyVaultExceptionTest { + + @Test + public void createWithMessage() { + final String msg = "msg"; + KeyVaultException exception = new KeyVaultException(msg); + + assertThat(exception).hasMessage(msg); + } + + @Test + public void createWithCause() { + Throwable cause = new Exception("cause"); + KeyVaultException exception = new KeyVaultException(cause); + + assertThat(exception).hasCause(cause); + } + +} diff --git a/key-vault/pom.xml b/key-vault/pom.xml index acbd0927b9..b05d4b032a 100644 --- a/key-vault/pom.xml +++ b/key-vault/pom.xml @@ -12,5 +12,6 @@ azure-key-vault key-vault-api + hashicorp-key-vault diff --git a/pom.xml b/pom.xml index 92896c2c13..961632632d 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ UTF-8 1.8 1.8 - 5.0.6.RELEASE + 5.1.2.RELEASE 1.7.5 2.7.3 1.14.0 @@ -220,6 +220,7 @@ com/quorum/tessera/config/util/ConsolePasswordReader* com/quorum/tessera/config/util/PasswordReaderFactory* com/quorum/tessera/key/vault/azure/AzureKeyVaultClientDelegate* + com/quorum/tessera/key/vault/hashicorp/KeyValueOperationsDelegateFactory* @@ -451,6 +452,12 @@ 0.8-SNAPSHOT + + com.quorum.tessera + hashicorp-key-vault + 0.8-SNAPSHOT + + com.quorum.tessera key-generation @@ -809,6 +816,30 @@ 1.3.2 + + com.microsoft.azure + azure-keyvault + 1.1.2 + + + + com.microsoft.azure + adal4j + 1.6.3 + + + + org.springframework.vault + spring-vault-core + 2.1.1.RELEASE + + + + com.squareup.okhttp3 + okhttp + 3.12.0 + + diff --git a/tessera-core/pom.xml b/tessera-core/pom.xml index 19066564eb..a3a3d5c751 100644 --- a/tessera-core/pom.xml +++ b/tessera-core/pom.xml @@ -57,6 +57,12 @@ + + com.quorum.tessera + hashicorp-key-vault + runtime + + javax.transaction javax.transaction-api