diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c231d..c5573dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## 0.6.0 [work in progress] +* [feature] Initialization from environment variables using `fromEnv()` in factory (#8) +* [feature] Automatic authentication with `buildAndAuth()` * [feature] Custom timeout and number of retries (#9) * [fix] `SecretResponse` does not throw NPE on `get(key)` and `getData()` diff --git a/pom.xml b/pom.xml index d8ce5e4..d96571e 100644 --- a/pom.xml +++ b/pom.xml @@ -72,5 +72,11 @@ 2.0.0.0 test + + com.github.stefanbirkner + system-rules + 1.16.0 + test + diff --git a/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java b/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java index b35233e..6e6fb38 100644 --- a/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java +++ b/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java @@ -17,6 +17,8 @@ package de.stklcode.jvault.connector.factory; import de.stklcode.jvault.connector.HTTPVaultConnector; +import de.stklcode.jvault.connector.VaultConnector; +import de.stklcode.jvault.connector.exception.ConnectionException; import de.stklcode.jvault.connector.exception.TlsException; import de.stklcode.jvault.connector.exception.VaultConnectorException; @@ -25,8 +27,11 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -39,6 +44,11 @@ import java.security.cert.X509Certificate; * @since 0.1 */ public class HTTPVaultConnectorFactory extends VaultConnectorFactory { + private static final String ENV_VAULT_ADDR = "VAULT_ADDR"; + private static final String ENV_VAULT_CACERT = "VAULT_CACERT"; + private static final String ENV_VAULT_TOKEN = "VAULT_TOKEN"; + private static final String ENV_VAULT_MAX_RETRIES = "VAULT_MAX_RETRIES"; + public static final String DEFAULT_HOST = "127.0.0.1"; public static final Integer DEFAULT_PORT = 8200; public static final boolean DEFAULT_TLS = true; @@ -52,6 +62,7 @@ public class HTTPVaultConnectorFactory extends VaultConnectorFactory { private SSLContext sslContext; private int numberOfRetries; private Integer timeout; + private String token; /** * Default empty constructor. @@ -154,6 +165,55 @@ public class HTTPVaultConnectorFactory extends VaultConnectorFactory { return this; } + /** + * Set token for automatic authentication, using {@link #buildAndAuth()}. + * + * @param token Vault token + * @return self + * @since 0.6.0 + */ + public HTTPVaultConnectorFactory withToken(String token) throws VaultConnectorException { + this.token = token; + return this; + } + + /** + * Build connector based on the {@code }VAULT_ADDR} and {@code VAULT_CACERT} (optional) environment variables. + * + * @return self + * @since 0.6.0 + */ + public HTTPVaultConnectorFactory fromEnv() throws VaultConnectorException { + /* Parse URL from environment variable */ + if (System.getenv(ENV_VAULT_ADDR) != null && !System.getenv(ENV_VAULT_ADDR).trim().isEmpty()) { + try { + URL url = new URL(System.getenv(ENV_VAULT_ADDR)); + this.host = url.getHost(); + this.port = url.getPort(); + this.tls = url.getProtocol().equals("https"); + } catch (MalformedURLException e) { + throw new ConnectionException("URL provided in environment variable malformed", e); + } + } + + /* Read number of retries */ + if (System.getenv(ENV_VAULT_MAX_RETRIES) != null) { + try { + numberOfRetries = Integer.parseInt(System.getenv(ENV_VAULT_MAX_RETRIES)); + } catch (NumberFormatException ignored) { + } + } + + /* Read token */ + token = System.getenv(ENV_VAULT_TOKEN); + + /* Parse certificate, if set */ + if (System.getenv(ENV_VAULT_CACERT) != null && !System.getenv(ENV_VAULT_CACERT).trim().isEmpty()) { + return withTrustedCA(Paths.get(System.getenv(ENV_VAULT_CACERT))); + } + return this; + } + /** * Define the number of retries to attempt on 5xx errors. * @@ -183,6 +243,15 @@ public class HTTPVaultConnectorFactory extends VaultConnectorFactory { return new HTTPVaultConnector(host, tls, port, prefix, sslContext, numberOfRetries, timeout); } + @Override + public HTTPVaultConnector buildAndAuth() throws VaultConnectorException { + if (token == null) + throw new ConnectionException("No vault token provided, unable to authenticate."); + HTTPVaultConnector con = new HTTPVaultConnector(host, tls, port, prefix, sslContext, numberOfRetries, timeout); + con.authToken(token); + return con; + } + /** * Create SSL Context trusting only provided certificate. * diff --git a/src/main/java/de/stklcode/jvault/connector/factory/VaultConnectorFactory.java b/src/main/java/de/stklcode/jvault/connector/factory/VaultConnectorFactory.java index efed35e..9d3ffd7 100644 --- a/src/main/java/de/stklcode/jvault/connector/factory/VaultConnectorFactory.java +++ b/src/main/java/de/stklcode/jvault/connector/factory/VaultConnectorFactory.java @@ -23,13 +23,14 @@ import de.stklcode.jvault.connector.exception.VaultConnectorException; * Abstract Vault Connector Factory interface. * Provides builder pattern style factory for Vault connectors. * - * @author Stefan Kalscheuer - * @since 0.1 + * @author Stefan Kalscheuer + * @since 0.1 */ public abstract class VaultConnectorFactory { /** * Get Factory implementation for HTTP Vault Connector - * @return HTTP Connector Factory + * + * @return HTTP Connector Factory */ public static HTTPVaultConnectorFactory httpFactory() { return new HTTPVaultConnectorFactory(); @@ -37,7 +38,16 @@ public abstract class VaultConnectorFactory { /** * Build command, produces connector after initialization. - * @return Vault Connector instance. + * + * @return Vault Connector instance. */ public abstract VaultConnector build(); + + /** + * Build connector and authenticate with token set in factory or from environment. + * + * @return Authenticated Vault connector instance. + * @since 0.6.0 + */ + public abstract VaultConnector buildAndAuth() throws VaultConnectorException; } diff --git a/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java b/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java new file mode 100644 index 0000000..b1629e0 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016-2017 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.factory; + +import de.stklcode.jvault.connector.HTTPVaultConnector; +import de.stklcode.jvault.connector.exception.TlsException; +import de.stklcode.jvault.connector.exception.VaultConnectorException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.NoSuchFileException; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +/** + * JUnit test for HTTP Vault connector factory + * + * @author Stefan Kalscheuer + * @since 0.6.0 + */ +public class HTTPVaultConnectorFactoryTest { + private static String VAULT_ADDR = "https://localhost:8201"; + private static Integer VAULT_MAX_RETRIES = 13; + private static String VAULT_TOKEN = "00001111-2222-3333-4444-555566667777"; + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + @Rule + public final EnvironmentVariables environment = new EnvironmentVariables(); + + /** + * Test building from environment variables + */ + @Test + public void testFromEnv() throws NoSuchFieldException, IllegalAccessException, IOException { + /* Provide address only should be enough */ + setenv(VAULT_ADDR, null, null, null); + + HTTPVaultConnectorFactory factory = null; + HTTPVaultConnector connector; + try { + factory = VaultConnectorFactory.httpFactory().fromEnv(); + } catch (VaultConnectorException e) { + fail("Factory creation from minimal environment failed"); + } + connector = factory.build(); + + assertThat("URL nor set correctly", getPrivate(connector, "baseURL"), is(equalTo(VAULT_ADDR + "/v1/"))); + assertThat("SSL context set when no cert provided", getPrivate(connector, "sslContext"), is(nullValue())); + assertThat("Non-default number of retries, when none set", getPrivate(connector, "retries"), is(0)); + + /* Provide address and number of retries */ + setenv(VAULT_ADDR, null, VAULT_MAX_RETRIES.toString(), null); + + try { + factory = VaultConnectorFactory.httpFactory().fromEnv(); + } catch (VaultConnectorException e) { + fail("Factory creation from environment failed"); + } + connector = factory.build(); + + assertThat("URL nor set correctly", getPrivate(connector, "baseURL"), is(equalTo(VAULT_ADDR + "/v1/"))); + assertThat("SSL context set when no cert provided", getPrivate(connector, "sslContext"), is(nullValue())); + assertThat("Number of retries not set correctly", getPrivate(connector, "retries"), is(VAULT_MAX_RETRIES)); + + /* Provide CA certificate */ + String VAULT_CACERT = tmpDir.newFolder().toString() + "/doesnotexist"; + setenv(VAULT_ADDR, VAULT_CACERT, VAULT_MAX_RETRIES.toString(), null); + + try { + VaultConnectorFactory.httpFactory().fromEnv(); + fail("Creation with unknown cert path failed."); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(TlsException.class))); + assertThat(e.getCause(), is(instanceOf(NoSuchFileException.class))); + assertThat(((NoSuchFileException)e.getCause()).getFile(), is(VAULT_CACERT)); + } + + /* Automatic authentication */ + setenv(VAULT_ADDR, null, VAULT_MAX_RETRIES.toString(), VAULT_TOKEN); + + try { + factory = VaultConnectorFactory.httpFactory().fromEnv(); + } catch (VaultConnectorException e) { + fail("Factory creation from minimal environment failed"); + } + assertThat("Token nor set correctly", getPrivate(factory, "token"), is(equalTo(VAULT_TOKEN))); + } + + private void setenv(String vault_addr, String vault_cacert, String vault_max_retries, String vault_token) { + environment.set("VAULT_ADDR", vault_addr); + environment.set("VAULT_CACERT", vault_cacert); + environment.set("VAULT_MAX_RETRIES", vault_max_retries); + environment.set("VAULT_TOKEN", vault_token); + } + + private Object getPrivate(Object target, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = target.getClass().getDeclaredField(fieldName); + if (field.isAccessible()) + return field.get(target); + field.setAccessible(true); + Object value = field.get(target); + field.setAccessible(false); + return value; + } +}