From 0c542c198c5b59810ecebcc9441443d6703b3b57 Mon Sep 17 00:00:00 2001 From: Max Lambrecht Date: Tue, 14 Jul 2020 15:50:53 -0300 Subject: [PATCH] Refactors and tests in java-spiffe-helper. Signed-off-by: Max Lambrecht --- .../java/io/spiffe/workloadapi/JwtSource.java | 2 - .../java/io/spiffe/helper/cli/Runner.java | 37 +++-- .../exception/KeyStoreHelperException.java | 15 ++ .../helper/exception/RunnerException.java | 14 ++ .../helper/keystore/KeyStoreHelper.java | 147 ++++++++++++------ .../helper/keystore/PrivateKeyEntry.java | 22 ++- .../java/io/spiffe/helper/cli/RunnerTest.java | 39 ++--- .../helper/keystore/KeyStoreHelperTest.java | 146 +++++++++++++++-- .../spiffe/helper/keystore/KeyStoreTest.java | 6 +- .../keystore/WorkloadApiClientErrorStub.java | 56 +++++++ .../io/spiffe/helper/utils/TestUtils.java | 53 +++++++ 11 files changed, 423 insertions(+), 114 deletions(-) create mode 100644 java-spiffe-helper/src/main/java/io/spiffe/helper/exception/KeyStoreHelperException.java create mode 100644 java-spiffe-helper/src/main/java/io/spiffe/helper/exception/RunnerException.java create mode 100644 java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientErrorStub.java create mode 100644 java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java diff --git a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/JwtSource.java b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/JwtSource.java index 69b100b..58d51a6 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/JwtSource.java +++ b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/JwtSource.java @@ -168,8 +168,6 @@ public class JwtSource implements JwtSvidSource, BundleSource, Closea } } - - private void init(final Duration timeout) throws TimeoutException { CountDownLatch done = new CountDownLatch(1); setJwtBundlesWatcher(done); diff --git a/java-spiffe-helper/src/main/java/io/spiffe/helper/cli/Runner.java b/java-spiffe-helper/src/main/java/io/spiffe/helper/cli/Runner.java index 1045a55..f6dfffb 100644 --- a/java-spiffe-helper/src/main/java/io/spiffe/helper/cli/Runner.java +++ b/java-spiffe-helper/src/main/java/io/spiffe/helper/cli/Runner.java @@ -1,6 +1,8 @@ package io.spiffe.helper.cli; import io.spiffe.exception.SocketEndpointAddressException; +import io.spiffe.helper.exception.KeyStoreHelperException; +import io.spiffe.helper.exception.RunnerException; import io.spiffe.helper.keystore.KeyStoreHelper; import io.spiffe.helper.keystore.KeyStoreType; import lombok.extern.java.Log; @@ -18,7 +20,6 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.KeyStoreException; import java.util.Properties; /** @@ -37,41 +38,45 @@ public class Runner { * * @param args contains the option with the config file path */ - public static void main(final String ...args) { + public static void main(final String ...args) throws RunnerException { String configFilePath = null; try { configFilePath = getCliConfigOption(args); val parameters = parseConfigFile(Paths.get(configFilePath)); val options = toKeyStoreOptions(parameters); - - // run KeyStoreHelper - new KeyStoreHelper(options); - } catch (IOException e) { - log.severe(String.format("Cannot open config file: %s %n %s", configFilePath, e.getMessage())); - } catch (KeyStoreException e) { - log.severe(String.format("Error storing certs in keystores: %s", e.getMessage())); - } catch (SocketEndpointAddressException e) { - log.severe(String.format("Workload API address is not valid: %s", e.getMessage())); - } catch (ParseException e) { - log.severe(String.format( "%s. Use -c, --config ", e.getMessage())); + try (val keyStoreHelper = KeyStoreHelper.create(options)) { + keyStoreHelper.run(true); + } + } catch (SocketEndpointAddressException | KeyStoreHelperException | RunnerException | IllegalArgumentException e) { + log.severe(e.getMessage()); + throw new RunnerException(e); } } - static Properties parseConfigFile(final Path configFilePath) throws IOException { + static Properties parseConfigFile(final Path configFilePath) throws RunnerException { Properties prop = new Properties(); try (InputStream in = Files.newInputStream(configFilePath)) { prop.load(in); + } catch (IOException e) { + val error = String.format("Cannot open config file: %s %n %s", configFilePath, e.getMessage()); + throw new RunnerException(error); } return prop; } - static String getCliConfigOption(final String ...args) throws ParseException { + static String getCliConfigOption(final String ...args) throws RunnerException { final Options cliOptions = new Options(); final Option confOption = new Option("c", "config", true, "config file"); confOption.setRequired(true); cliOptions.addOption(confOption); CommandLineParser parser = new DefaultParser(); - CommandLine cmd = parser.parse(cliOptions, args); + CommandLine cmd = null; + try { + cmd = parser.parse(cliOptions, args); + } catch (ParseException e) { + val error = String.format( "%s. Use -c, --config ", e.getMessage()); + throw new RunnerException(error); + } return cmd.getOptionValue("config"); } diff --git a/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/KeyStoreHelperException.java b/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/KeyStoreHelperException.java new file mode 100644 index 0000000..e169d5b --- /dev/null +++ b/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/KeyStoreHelperException.java @@ -0,0 +1,15 @@ +package io.spiffe.helper.exception; + +/** + * Checked exception to be thrown when there is an error creating or initializing a KeyStoreHelper. + */ +public class KeyStoreHelperException extends Exception { + + public KeyStoreHelperException(String message) { + super(message); + } + + public KeyStoreHelperException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/RunnerException.java b/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/RunnerException.java new file mode 100644 index 0000000..4860f9c --- /dev/null +++ b/java-spiffe-helper/src/main/java/io/spiffe/helper/exception/RunnerException.java @@ -0,0 +1,14 @@ +package io.spiffe.helper.exception; + +/** + * Checked exception to be thrown when there are errors configuring the cli Runner. + */ +public class RunnerException extends Exception { + public RunnerException(String message) { + super(message); + } + + public RunnerException(Throwable cause) { + super(cause); + } +} diff --git a/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/KeyStoreHelper.java b/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/KeyStoreHelper.java index 81868f3..1d6286f 100644 --- a/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/KeyStoreHelper.java +++ b/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/KeyStoreHelper.java @@ -2,6 +2,8 @@ package io.spiffe.helper.keystore; import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.exception.SocketEndpointAddressException; +import io.spiffe.exception.WatcherException; +import io.spiffe.helper.exception.KeyStoreHelperException; import io.spiffe.spiffeid.TrustDomain; import io.spiffe.workloadapi.DefaultWorkloadApiClient; import io.spiffe.workloadapi.Watcher; @@ -53,90 +55,131 @@ public class KeyStoreHelper implements Closeable { private final WorkloadApiClient workloadApiClient; + private volatile boolean closed; + private volatile CountDownLatch countDownLatch; /** - * Constructor. - *

* Creates an instance of a KeyStoreHelper for fetching X.509 SVIDs and bundles * from a Workload API and store them in a binary Java KeyStore in disk. - *

- * It blocks until the initial update has been received from the Workload API. * * @param options an instance of {@link KeyStoreOptions} - * @throws SocketEndpointAddressException is the socket endpoint address is not valid - * @throws KeyStoreException is the entry cannot be stored in the KeyStore + * @return an instance of a KeyStoreHelper + * @throws SocketEndpointAddressException if the socket endpoint address is not valid + * @throws KeyStoreHelperException if the KeyStoreHelper cannot be created */ - public KeyStoreHelper(@NonNull final KeyStoreOptions options) - throws SocketEndpointAddressException, KeyStoreException { - - final KeyStoreType keyStoreType; - if (options.keyStoreType == null) { - keyStoreType = KeyStoreType.getDefaultType(); - } else { - keyStoreType = options.keyStoreType; - } - - this.keyPass = options.keyPass; - - if (StringUtils.isBlank(options.keyAlias)) { - this.keyAlias = DEFAULT_ALIAS; - } else { - this.keyAlias = options.keyAlias; - } + public static KeyStoreHelper create(@NonNull final KeyStoreOptions options) throws SocketEndpointAddressException, KeyStoreHelperException { if (options.keyStorePath.equals(options.trustStorePath)) { - throw new KeyStoreException("KeyStore and TrustStore should use different files"); + throw new KeyStoreHelperException("KeyStore and TrustStore should use different files"); } - this.keyStore = KeyStore.builder() - .keyStoreFilePath(options.keyStorePath) - .keyStoreType(keyStoreType) - .keyStorePassword(options.keyStorePass) - .build(); - - this.trustStore = KeyStore.builder() - .keyStoreFilePath(options.trustStorePath) - .keyStoreType(keyStoreType) - .keyStorePassword(options.trustStorePass) - .build(); - - if (options.workloadApiClient != null) { - workloadApiClient = options.workloadApiClient; - } else { - workloadApiClient = createNewClient(options.spiffeSocketPath); + if (options.keyStoreType == null) { + options.keyStoreType = KeyStoreType.getDefaultType(); } - setX509ContextWatcher(workloadApiClient); + if (StringUtils.isBlank(options.keyAlias)) { + options.keyAlias = DEFAULT_ALIAS; + } + + val keyStore = createKeyStore(options, options.keyStorePath, options.keyStorePass); + val trustStore = createKeyStore(options, options.trustStorePath, options.trustStorePass); + + if (options.workloadApiClient == null) { + options.workloadApiClient = createNewClient(options.spiffeSocketPath); + } + + return new KeyStoreHelper(keyStore, trustStore, options.keyPass, options.keyAlias, options.workloadApiClient); } + /** + * Sets the instance to run fetching and storing the X.509 SVIDs and Bundles. + * + * @param keepRunning if true, the process will block receiving and storing updates, otherwise it blocks only until + * the first X.509 context is received and stored. + * @throws KeyStoreHelperException if there is an error fetching or storing the X.509 SVIDs and Bundles + */ + public void run(boolean keepRunning) throws KeyStoreHelperException { + if (isClosed()) { + throw new IllegalStateException("KeyStoreHelper is closed"); + } + + try { + this.setX509ContextWatcher(keepRunning); + } catch (Exception e) { + throw new KeyStoreHelperException("Error running KeyStoreHelper", e); + } + } + + /** + * Closes the KeyStoreHelper instance. + */ @SneakyThrows @Override public void close() { - workloadApiClient.close(); + if (!closed) { + synchronized (this) { + if (!closed) { + workloadApiClient.close(); + countDown(); + closed = true; + log.info("KeyStoreHelper is closed"); + } + } + } } + private void countDown() { + if (countDownLatch != null) { + countDownLatch.countDown(); + } + } - private WorkloadApiClient createNewClient(final String spiffeSocketPath) throws SocketEndpointAddressException { + private KeyStoreHelper(KeyStore keyStore, KeyStore trustStore, String keyPass, String keyAlias, WorkloadApiClient workloadApiClient) { + this.keyStore = keyStore; + this.trustStore = trustStore; + this.keyPass = keyPass; + this.keyAlias = keyAlias; + this.workloadApiClient = workloadApiClient; + } + + private static KeyStore createKeyStore(KeyStoreOptions options, Path keyStorePath, String keyStorePass) throws KeyStoreHelperException { + try { + return KeyStore.builder() + .keyStoreFilePath(keyStorePath) + .keyStoreType(options.keyStoreType) + .keyStorePassword(keyStorePass) + .build(); + } catch (KeyStoreException e) { + throw new KeyStoreHelperException("Error creating KeyStore/TrustStore", e); + } + } + + private static WorkloadApiClient createNewClient(final String spiffeSocketPath) throws SocketEndpointAddressException { val clientOptions = DefaultWorkloadApiClient.ClientOptions.builder().spiffeSocketPath(spiffeSocketPath).build(); return DefaultWorkloadApiClient.newClient(clientOptions); } - private void setX509ContextWatcher(final WorkloadApiClient workloadApiClient) { - val countDownLatch = new CountDownLatch(1); + private void setX509ContextWatcher(boolean keepRunning) { + countDownLatch = new CountDownLatch(1); workloadApiClient.watchX509Context(new Watcher() { @Override public void onUpdate(X509Context update) { try { storeX509ContextUpdate(update); + if (!keepRunning) { + // got a X509 update, process is complete + countDownLatch.countDown(); + } } catch (KeyStoreException e) { this.onError(e); } - countDownLatch.countDown(); } @Override - public void onError(Throwable t) { - log.log(Level.SEVERE, "Error processing X.509 context update", t); + public void onError(Throwable e) { + log.log(Level.SEVERE, e.getMessage()); + countDownLatch.countDown(); + throw new WatcherException("Error processing X.509 context update", e); } }); @@ -177,6 +220,12 @@ public class KeyStoreHelper implements Closeable { return trustDomain.getName().concat(".").concat(String.valueOf(index)); } + private boolean isClosed() { + synchronized (this) { + return closed; + } + } + private void await(final CountDownLatch latch) { try { latch.await(); @@ -200,7 +249,7 @@ public class KeyStoreHelper implements Closeable { * for information about standard keystore types. *

* The same type is used for both the KeyStore and the TrustStore. - * + *

* Optional. Default is PKCS12. *

* keyStorePass The password to generate the keystore integrity check. diff --git a/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/PrivateKeyEntry.java b/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/PrivateKeyEntry.java index 1dad7f8..72e69d4 100644 --- a/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/PrivateKeyEntry.java +++ b/java-spiffe-helper/src/main/java/io/spiffe/helper/keystore/PrivateKeyEntry.java @@ -1,17 +1,27 @@ package io.spiffe.helper.keystore; +import lombok.AccessLevel; import lombok.Builder; -import lombok.Value; +import lombok.Data; +import lombok.Setter; import java.security.Key; import java.security.cert.X509Certificate; -@Value +@Data class PrivateKeyEntry { - String alias; - Key privateKey; - String password; - X509Certificate[] certificateChain; + + @Setter(AccessLevel.NONE) + private String alias; + + @Setter(AccessLevel.NONE) + private Key privateKey; + + @Setter(AccessLevel.NONE) + private String password; + + @Setter(AccessLevel.NONE) + private X509Certificate[] certificateChain; @Builder PrivateKeyEntry( diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java index cc6579c..7ad3b99 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java @@ -1,17 +1,16 @@ package io.spiffe.helper.cli; import io.spiffe.exception.SocketEndpointAddressException; -import org.apache.commons.cli.ParseException; +import io.spiffe.helper.exception.RunnerException; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyStoreException; import java.util.Properties; +import static io.spiffe.helper.utils.TestUtils.toUri; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -23,8 +22,8 @@ class RunnerTest { try { Runner.main("-c", path.toString()); fail("expected exception: property is missing"); - } catch (IllegalArgumentException e) { - assertEquals("keyStorePath config is missing", e.getMessage()); + } catch (RunnerException e) { + assertEquals("keyStorePath config is missing", e.getCause().getMessage()); } } @@ -34,8 +33,8 @@ class RunnerTest { try { Runner.main("-c", path.toString()); fail("expected exception: property is missing"); - } catch (IllegalArgumentException e) { - assertEquals("keyStorePass config is missing", e.getMessage()); + } catch (RunnerException e) { + assertEquals("keyStorePass config is missing", e.getCause().getMessage()); } } @@ -45,8 +44,8 @@ class RunnerTest { try { Runner.main("-c", path.toString()); fail("expected exception: property is missing"); - } catch (IllegalArgumentException e) { - assertEquals("keyPass config is missing", e.getMessage()); + } catch (RunnerException e) { + assertEquals("keyPass config is missing", e.getCause().getMessage()); } } @@ -56,8 +55,8 @@ class RunnerTest { try { Runner.main("-c", path.toString()); fail("expected exception: property is missing"); - } catch (IllegalArgumentException e) { - assertEquals("trustStorePath config is missing", e.getMessage()); + } catch (RunnerException e) { + assertEquals("trustStorePath config is missing", e.getCause().getMessage()); } } @@ -67,8 +66,8 @@ class RunnerTest { try { Runner.main("-c", path.toString()); fail("expected exception: property is missing"); - } catch (IllegalArgumentException e) { - assertEquals("trustStorePass config is missing", e.getMessage()); + } catch (RunnerException e) { + assertEquals("trustStorePass config is missing", e.getCause().getMessage()); } } @@ -77,7 +76,7 @@ class RunnerTest { String option = null; try { option = Runner.getCliConfigOption("-c", "example"); - } catch (ParseException e) { + } catch (RunnerException e) { fail(e); } assertEquals("example", option); @@ -88,7 +87,7 @@ class RunnerTest { String option = null; try { option = Runner.getCliConfigOption("--config", "example"); - } catch (ParseException e) { + } catch (RunnerException e) { fail(e); } assertEquals("example", option); @@ -99,13 +98,13 @@ class RunnerTest { try { Runner.getCliConfigOption("--unknown", "example"); fail("expected parse exception"); - } catch (ParseException e) { - assertEquals("Unrecognized option: --unknown", e.getMessage()); + } catch (RunnerException e) { + assertEquals("Unrecognized option: --unknown. Use -c, --config ", e.getMessage()); } } @Test - void test_ParseConfigFile() throws URISyntaxException, IOException { + void test_ParseConfigFile() throws URISyntaxException, RunnerException { final Path path = Paths.get(toUri("testdata/cli/correct.conf")); final Properties properties = Runner.parseConfigFile(path); @@ -118,8 +117,4 @@ class RunnerTest { assertEquals("other_alias", properties.getProperty("keyAlias")); assertEquals("unix:/tmp/agent.sock", properties.getProperty("spiffeSocketPath")); } - - private URI toUri(String path) throws URISyntaxException { - return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); - } } \ No newline at end of file diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java index 24be0e1..c4f26b0 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java @@ -1,8 +1,11 @@ package io.spiffe.helper.keystore; import io.spiffe.exception.SocketEndpointAddressException; +import io.spiffe.helper.exception.KeyStoreHelperException; +import io.spiffe.helper.utils.TestUtils; import io.spiffe.internal.CertificateUtils; import io.spiffe.spiffeid.SpiffeId; +import io.spiffe.workloadapi.Address; import io.spiffe.workloadapi.WorkloadApiClient; import lombok.SneakyThrows; import lombok.val; @@ -29,16 +32,25 @@ import static org.junit.jupiter.api.Assertions.fail; class KeyStoreHelperTest { private WorkloadApiClient workloadApiClient; + private WorkloadApiClient workloadApiClientErrorStub; private Path keyStoreFilePath; private Path trustStoreFilePath; @BeforeEach void setUp() { workloadApiClient = new WorkloadApiClientStub(); + workloadApiClientErrorStub = new WorkloadApiClientErrorStub(); + } + + @SneakyThrows + @AfterEach + void tearDown() { + deleteFile(keyStoreFilePath); + deleteFile(trustStoreFilePath); } @Test - void testNewHelper_certs_are_stored_successfully() throws KeyStoreException, SocketEndpointAddressException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, IOException { + void testNewHelper_certs_are_stored_successfully() throws SocketEndpointAddressException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException { val keyStorefileName = RandomStringUtils.randomAlphabetic(10); keyStoreFilePath = Paths.get(keyStorefileName); @@ -65,7 +77,11 @@ class KeyStoreHelperTest { .build(); // run KeyStoreHelper - new KeyStoreHelper(options); + try (val keystoreHelper = KeyStoreHelper.create(options)) { + keystoreHelper.run(false); + } catch (KeyStoreHelperException e) { + fail(); + } checkPrivateKeyEntry(keyStoreFilePath, keyStorePass, keyPass, keyStoreType, alias); val authority1Alias = "example.org.0"; @@ -95,8 +111,11 @@ class KeyStoreHelperTest { .workloadApiClient(workloadApiClient) .build(); - // run KeyStoreHelper - new KeyStoreHelper(options); + try (val keystoreHelper = KeyStoreHelper.create(options)) { + keystoreHelper.run(false); + } catch (KeyStoreHelperException e) { + fail(); + } checkPrivateKeyEntry(keyStoreFilePath, keyStorePass, keyPass, KeyStoreType.getDefaultType(), KeyStoreHelper.DEFAULT_ALIAS); val authority1Alias = "example.org.0"; @@ -104,7 +123,7 @@ class KeyStoreHelperTest { } @Test - void testNewHelper_keyStore_trustStore_same_file_throwsException() throws SocketEndpointAddressException { + void testCreateHelper_keyStore_trustStore_same_file_throwsException() throws SocketEndpointAddressException { val keyStorefileName = RandomStringUtils.randomAlphabetic(10); keyStoreFilePath = Paths.get(keyStorefileName); @@ -126,22 +145,121 @@ class KeyStoreHelperTest { .build(); try { - new KeyStoreHelper(options); + KeyStoreHelper.create(options); fail("expected exception: KeyStore and TrustStore should use different files"); - } catch (KeyStoreException e) { + } catch (KeyStoreHelperException e) { assertEquals("KeyStore and TrustStore should use different files", e.getMessage()); } - } - @SneakyThrows - @AfterEach - void tearDown() { - deleteFile(keyStoreFilePath); - deleteFile(trustStoreFilePath); - workloadApiClient.close(); + @Test + void testCreateHelper_keyStore_cannotStoreCerts() throws SocketEndpointAddressException { + + keyStoreFilePath = Paths.get("dummy://invalid"); + trustStoreFilePath = Paths.get("dummy://otherinvalid"); + + val trustStorePass = "truststore123"; + val keyStorePass = "keystore123"; + val keyPass = "keypass123"; + val alias = "other_alias"; + + final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions + .builder() + .keyStorePath(keyStoreFilePath) + .keyStorePass(keyStorePass) + .trustStorePath(trustStoreFilePath) + .trustStorePass(trustStorePass) + .keyPass(keyPass) + .keyAlias(alias) + .workloadApiClient(workloadApiClient) + .build(); + + try { + KeyStoreHelper helper = KeyStoreHelper.create(options); + helper.run(false); + fail(); + } catch (KeyStoreHelperException e) { + assertEquals("Error running KeyStoreHelper", e.getMessage()); + assertEquals("java.nio.file.NoSuchFileException: dummy:/invalid", e.getCause().getCause().getMessage()); + } } + @Test + void testCreateKeyStoreHelper_createNewClient() throws Exception { + TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/test"); + val options = getKeyStoreValidOptions(null); + try { + KeyStoreHelper.create(options); + } catch (KeyStoreHelperException e) { + fail(); + } + } + + @Test + void testCreateKeyStoreHelper_nullParameter() { + try { + KeyStoreHelper.create(null); + } catch (NullPointerException e) { + assertEquals("options is marked non-null but is null", e.getMessage()); + } catch (SocketEndpointAddressException | KeyStoreHelperException e) { + fail(); + } + } + + @Test + void testCreateKeyStoreHelper_cannotRunIfClosed() throws Exception { + val options = getKeyStoreValidOptions(workloadApiClient); + try { + KeyStoreHelper helper = KeyStoreHelper.create(options); + helper.close(); + helper.run(true); + } catch (IllegalStateException e) { + assertEquals("KeyStoreHelper is closed", e.getMessage()); + } catch (KeyStoreHelperException e) { + fail(); + } + } + + @Test + void testCreateKeyStoreHelper_throwsExceptionWhenNoUpdateCanBeFetched() throws Exception { + val options = getKeyStoreValidOptions(workloadApiClientErrorStub); + try { + KeyStoreHelper helper = KeyStoreHelper.create(options); + helper.run(false); + fail(); + } catch (KeyStoreHelperException e) { + assertEquals("Error running KeyStoreHelper", e.getMessage()); + } + } + + private KeyStoreHelper.KeyStoreOptions getKeyStoreValidOptions(WorkloadApiClient workloadApiClient) { + val keyStorefileName = RandomStringUtils.randomAlphabetic(10); + keyStoreFilePath = Paths.get(keyStorefileName); + val trustStorefileName = RandomStringUtils.randomAlphabetic(10); + trustStoreFilePath = Paths.get(trustStorefileName); + + val trustStorePass = "truststore123"; + val keyStorePass = "keystore123"; + val keyPass = "keypass123"; + val alias = "other_alias"; + + val options = KeyStoreHelper.KeyStoreOptions + .builder() + .keyStorePath(keyStoreFilePath) + .keyStorePass(keyStorePass) + .trustStorePath(trustStoreFilePath) + .trustStorePass(trustStorePass) + .keyPass(keyPass) + .keyAlias(alias); + + if (workloadApiClient != null) { + options.workloadApiClient(workloadApiClient); + } + + return options.build(); + } + + private void checkPrivateKeyEntry(Path keyStoreFilePath, String keyStorePassword, String privateKeyPassword, diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java index 25b41e2..00422c0 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; @@ -25,6 +24,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import static io.spiffe.helper.utils.TestUtils.toUri; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -222,10 +222,6 @@ public class KeyStoreTest { assertEquals(SpiffeId.parse("spiffe://example.org"), spiffeId); } - private URI toUri(String path) throws URISyntaxException { - return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); - } - private void deleteFile(Path filePath) { try { Files.delete(filePath); diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientErrorStub.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientErrorStub.java new file mode 100644 index 0000000..dae076f --- /dev/null +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientErrorStub.java @@ -0,0 +1,56 @@ +package io.spiffe.helper.keystore; + +import io.spiffe.bundle.jwtbundle.JwtBundleSet; +import io.spiffe.exception.JwtBundleException; +import io.spiffe.exception.JwtSvidException; +import io.spiffe.exception.X509ContextException; +import io.spiffe.spiffeid.SpiffeId; +import io.spiffe.svid.jwtsvid.JwtSvid; +import io.spiffe.workloadapi.Watcher; +import io.spiffe.workloadapi.WorkloadApiClient; +import io.spiffe.workloadapi.X509Context; +import lombok.NonNull; + +import java.io.IOException; + +public class WorkloadApiClientErrorStub implements WorkloadApiClient { + + @Override + public X509Context fetchX509Context() throws X509ContextException { + throw new X509ContextException("Testing exception"); + } + + @Override + public void watchX509Context(@NonNull final Watcher watcher) { + watcher.onError(new X509ContextException("Testing exception")); + } + + @Override + public JwtSvid fetchJwtSvid(@NonNull final String audience, final String... extraAudience) throws JwtSvidException { + throw new JwtSvidException("Testing exception"); + } + + @Override + public JwtSvid fetchJwtSvid(@NonNull final SpiffeId subject, @NonNull final String audience, final String... extraAudience) throws JwtSvidException { + throw new JwtSvidException("Testing exception"); + } + + @Override + public JwtBundleSet fetchJwtBundles() throws JwtBundleException { + throw new JwtBundleException("Testing exception"); + } + + @Override + public JwtSvid validateJwtSvid(@NonNull final String token, @NonNull final String audience) throws JwtSvidException { + return null; + } + + @Override + public void watchJwtBundles(@NonNull final Watcher watcher) { + watcher.onError(new JwtBundleException("Testing exception")); + } + + @Override + public void close() throws IOException { + } +} diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java new file mode 100644 index 0000000..20529bc --- /dev/null +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java @@ -0,0 +1,53 @@ +package io.spiffe.helper.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +/** + * Util methods for testing. + */ +public class TestUtils { + + private TestUtils() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static void setEnvironmentVariable(String variableName, String value) throws Exception { + Class processEnvironment = Class.forName("java.lang.ProcessEnvironment"); + + Field unmodifiableMapField = getField(processEnvironment, "theUnmodifiableEnvironment"); + Object unmodifiableMap = unmodifiableMapField.get(null); + injectIntoUnmodifiableMap(variableName, value, unmodifiableMap); + + Field mapField = getField(processEnvironment, "theEnvironment"); + Map map = (Map) mapField.get(null); + map.put(variableName, value); + } + + public static Object invokeMethod(Class clazz, String methodName, Object... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = clazz.getDeclaredMethod(methodName); + method.setAccessible(true); + return method.invoke(args); + } + + public static Field getField(Class clazz, String fieldName) throws NoSuchFieldException { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } + + private static void injectIntoUnmodifiableMap(String key, String value, Object map) throws ReflectiveOperationException { + Class unmodifiableMap = Class.forName("java.util.Collections$UnmodifiableMap"); + Field field = getField(unmodifiableMap, "m"); + Object obj = field.get(map); + ((Map) obj).put(key, value); + } + + public static URI toUri(String path) throws URISyntaxException { + return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); + } +}