Add Vault external configuration supplier
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/78339994 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/78339994 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/78339994 Branch: refs/heads/master Commit: 783399941cdc508f696dbc70360be5c38c08a3c3 Parents: 9bb0d71 Author: Richard Downer <[email protected]> Authored: Thu Nov 5 16:07:54 2015 +0000 Committer: Richard Downer <[email protected]> Committed: Thu Nov 5 16:07:54 2015 +0000 ---------------------------------------------------------------------- .../vault/VaultAppIdExternalConfigSupplier.java | 71 +++++++++ .../vault/VaultExternalConfigSupplier.java | 114 ++++++++++++++ .../vault/VaultTokenExternalConfigSupplier.java | 23 +++ .../VaultUserPassExternalConfigSupplier.java | 39 +++++ .../VaultExternalConfigSupplierLiveTest.java | 151 +++++++++++++++++++ 5 files changed, 398 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/78339994/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultAppIdExternalConfigSupplier.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultAppIdExternalConfigSupplier.java b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultAppIdExternalConfigSupplier.java new file mode 100644 index 0000000..6e9cd8b --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultAppIdExternalConfigSupplier.java @@ -0,0 +1,71 @@ +package org.apache.brooklyn.core.config.external.vault; + +import com.google.api.client.util.Lists; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.List; +import java.util.Map; + +public class VaultAppIdExternalConfigSupplier extends VaultExternalConfigSupplier { + + private static final Logger LOG = LoggerFactory.getLogger(VaultAppIdExternalConfigSupplier.class); + + protected VaultAppIdExternalConfigSupplier(ManagementContext managementContext, String name, Map<String, String> config) { + super(managementContext, name, config); + } + + protected String initAndLogIn(Map<String, String> config) { + List<String> errors = Lists.newArrayListWithCapacity(1); + String appId = config.get("appId"); + if (Strings.isBlank(appId)) errors.add("missing configuration 'appId'"); + if (!errors.isEmpty()) { + String message = String.format("Problem configuration Vault external config supplier '%s': %s", + name, Joiner.on(System.lineSeparator()).join(errors)); + throw new IllegalArgumentException(message); + } + + String userId = getUserId(config); + + LOG.info("Config supplier named {} using Vault at {} appID {} userID {} path {}", new Object[] { + name, endpoint, appId, userId, path }); + + String path = "v1/auth/app-id/login"; + ImmutableMap<String, String> requestData = ImmutableMap.of("app_id", appId, "user_id", userId); + ImmutableMap<String, String> headers = MINIMAL_HEADERS; + JsonObject response = apiPost(path, headers, requestData); + return response.getAsJsonObject("auth").get("client_token").getAsString(); + } + + private String getUserId(Map<String, String> config) { + String userId = config.get("userId"); + if (Strings.isBlank(userId)) + userId = getUserIdFromMacAddress(); + return userId; + } + + private static String getUserIdFromMacAddress() { + byte[] mac; + try { + InetAddress ip = InetAddress.getLocalHost(); + NetworkInterface network = NetworkInterface.getByInetAddress(ip); + mac = network.getHardwareAddress(); + } catch (Throwable t) { + throw Exceptions.propagate(t); + } + StringBuilder sb = new StringBuilder(); + for (byte aMac : mac) { + sb.append(String.format("%02x", aMac)); + } + return sb.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/78339994/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplier.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplier.java b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplier.java new file mode 100644 index 0000000..031ee8e --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplier.java @@ -0,0 +1,114 @@ +package org.apache.brooklyn.core.config.external.vault; + +import com.google.api.client.util.Lists; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.config.external.AbstractExternalConfigSupplier; +import org.apache.brooklyn.util.core.http.HttpTool; +import org.apache.brooklyn.util.core.http.HttpToolResponse; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.text.Strings; +import org.apache.http.client.HttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; + +public abstract class VaultExternalConfigSupplier extends AbstractExternalConfigSupplier { + public static final String CHARSET_NAME = "UTF-8"; + public static final ImmutableMap<String, String> MINIMAL_HEADERS = ImmutableMap.of( + "Content-Type", "application/json; charset=" + CHARSET_NAME, + "Accept", "application/json", + "Accept-Charset", CHARSET_NAME); + private static final Logger LOG = LoggerFactory.getLogger(VaultExternalConfigSupplier.class); + protected final Map<String, String> config; + protected final String name; + protected final HttpClient httpClient; + protected final Gson gson; + protected final String endpoint; + protected final String path; + protected final String token; + protected final ImmutableMap<String, String> headersWithToken; + + public VaultExternalConfigSupplier(ManagementContext managementContext, String name, Map<String, String> config) { + super(managementContext, name); + this.config = config; + this.name = name; + httpClient = HttpTool.httpClientBuilder().build(); + gson = new GsonBuilder().create(); + + List<String> errors = Lists.newArrayListWithCapacity(2); + endpoint = config.get("endpoint"); + if (Strings.isBlank(endpoint)) errors.add("missing configuration 'endpoint'"); + path = config.get("path"); + if (Strings.isBlank(path)) errors.add("missing configuration 'path'"); + if (!errors.isEmpty()) { + String message = String.format("Problem configuration Vault external config supplier '%s': %s", + name, Joiner.on(System.lineSeparator()).join(errors)); + throw new IllegalArgumentException(message); + } + + token = initAndLogIn(config); + headersWithToken = ImmutableMap.<String, String>builder() + .putAll(MINIMAL_HEADERS) + .put("X-Vault-Token", token) + .build(); + } + + protected abstract String initAndLogIn(Map<String, String> config); + + @Override + public String get(String key) { + JsonObject response = apiGet(Urls.mergePaths("v1", path), headersWithToken); + return response.getAsJsonObject("data").get(key).getAsString(); + } + + protected JsonObject apiGet(String path, ImmutableMap<String, String> headers) { + try { + String uri = Urls.mergePaths(endpoint, path); + LOG.info("Vault request - GET: {}", uri); + LOG.info("Vault request - headers: {}", headers.toString()); + HttpToolResponse response = HttpTool.httpGet(httpClient, Urls.toUri(uri), headers); + LOG.info("Vault response - code: {} {}", new Object[]{Integer.toString(response.getResponseCode()), response.getReasonPhrase()}); + LOG.info("Vault response - headers: {}", response.getHeaderLists().toString()); + String responseBody = new String(response.getContent(), CHARSET_NAME); + LOG.info("Vault response - body: {}", responseBody); + if (HttpTool.isStatusCodeHealthy(response.getResponseCode())) { + return gson.fromJson(responseBody, JsonObject.class); + } else { + throw new IllegalStateException("HTTP request returned error"); + } + } catch (UnsupportedEncodingException e) { + throw Exceptions.propagate(e); + } + } + + protected JsonObject apiPost(String path, ImmutableMap<String, String> headers, ImmutableMap<String, String> requestData) { + try { + String body = gson.toJson(requestData); + String uri = Urls.mergePaths(endpoint, path); + LOG.info("Vault request - POST: {}", uri); + LOG.info("Vault request - headers: {}", headers.toString()); + LOG.info("Vault request - body: {}", body); + HttpToolResponse response = HttpTool.httpPost(httpClient, Urls.toUri(uri), headers, body.getBytes(CHARSET_NAME)); + LOG.info("Vault response - code: {} {}", new Object[]{Integer.toString(response.getResponseCode()), response.getReasonPhrase()}); + LOG.info("Vault response - headers: {}", response.getHeaderLists().toString()); + String responseBody = new String(response.getContent(), CHARSET_NAME); + LOG.info("Vault response - body: {}", responseBody); + if (HttpTool.isStatusCodeHealthy(response.getResponseCode())) { + return gson.fromJson(responseBody, JsonObject.class); + } else { + throw new IllegalStateException("HTTP request returned error"); + } + } catch (UnsupportedEncodingException e) { + throw Exceptions.propagate(e); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/78339994/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultTokenExternalConfigSupplier.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultTokenExternalConfigSupplier.java b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultTokenExternalConfigSupplier.java new file mode 100644 index 0000000..ed994b4 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultTokenExternalConfigSupplier.java @@ -0,0 +1,23 @@ +package org.apache.brooklyn.core.config.external.vault; + +import com.google.common.collect.ImmutableMap; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; +import org.apache.brooklyn.util.text.Strings; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; + +public class VaultTokenExternalConfigSupplier extends VaultExternalConfigSupplier { + public VaultTokenExternalConfigSupplier(ManagementContext managementContext, String name, Map<String, String> config) { + super(managementContext, name, config); + } + + @Override + protected String initAndLogIn(Map<String, String> config) { + String tokenProperty = config.get("token"); + checkArgument(Strings.isNonBlank(tokenProperty), "property not set: token"); + return tokenProperty; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/78339994/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultUserPassExternalConfigSupplier.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultUserPassExternalConfigSupplier.java b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultUserPassExternalConfigSupplier.java new file mode 100644 index 0000000..c43ab90 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/config/external/vault/VaultUserPassExternalConfigSupplier.java @@ -0,0 +1,39 @@ +package org.apache.brooklyn.core.config.external.vault; + +import com.google.api.client.util.Lists; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.util.text.Strings; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; + +public class VaultUserPassExternalConfigSupplier extends VaultExternalConfigSupplier { + public VaultUserPassExternalConfigSupplier(ManagementContext managementContext, String name, Map<String, String> config) { + super(managementContext, name, config); + } + + @Override + protected String initAndLogIn(Map<String, String> config) { + List<String> errors = Lists.newArrayListWithCapacity(2); + String username = config.get("username"); + if (Strings.isBlank(username)) errors.add("missing configuration 'username'"); + String password = config.get("password"); + if (Strings.isBlank(username)) errors.add("missing configuration 'password'"); + if (!errors.isEmpty()) { + String message = String.format("Problem configuration Vault external config supplier '%s': %s", + name, Joiner.on(System.lineSeparator()).join(errors)); + throw new IllegalArgumentException(message); + } + + String path = "v1/auth/userpass/login/" + username; + ImmutableMap<String, String> requestData = ImmutableMap.of("password", password); + ImmutableMap<String, String> headers = MINIMAL_HEADERS; + JsonObject response = apiPost(path, headers, requestData); + return response.getAsJsonObject("auth").get("client_token").getAsString(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/78339994/core/src/test/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplierLiveTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplierLiveTest.java b/core/src/test/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplierLiveTest.java new file mode 100644 index 0000000..a3b7eae --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/config/external/vault/VaultExternalConfigSupplierLiveTest.java @@ -0,0 +1,151 @@ +package org.apache.brooklyn.core.config.external.vault; + +import com.google.common.collect.ImmutableMap; +import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; +import org.apache.brooklyn.util.text.Strings; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +/** + * Test the operation of the Vault external configuration supplier. + * + * <p>To run this test, you must have a working Vault server, and set a number of properties to allow the test to + * query your Vault server.</p> + * + * <p>You should start, initialise and unseal your vault according to the Vault documentation. Then you should insert + * a secret into Vault:</p> + * + * <p><tt>vault write secret/test password=foobar</tt></p> + * + * <p>Then set system properties so that the test can reach Vault and knows about the secret:</p> + * + * <p><tt>-Dtest.brooklyn.vault.endpoint=http://127.0.0.1:8200<br /> + * -Dtest.brooklyn.vault.path=secret/test<br /> + * -Dtest.brooklyn.vault.propertyName=password<br /> + * -Dtest.brooklyn.vault.propertyExpectedValue=foobar</tt></p> + * + * <p>You will also need to configure authentication methods for the individual tests. Refer to the "see also" section + * to find each method that needs further configuration.</p> + * + * @see #testAppIdAuthenticationWithAutomaticUserId() + */ +public class VaultExternalConfigSupplierLiveTest { + + private String endpoint; + private String path; + private String propertyName; + private String propertyExpectedValue; + + @BeforeClass + public void setUp() throws Exception { + endpoint = getTestProperty("endpoint"); + path = getTestProperty("path"); + propertyName = getTestProperty("propertyName"); + propertyExpectedValue = getTestProperty("propertyExpectedValue"); + } + + private String getTestProperty(String name) { + String propName = "test.brooklyn.vault." + name; + String propVal = System.getProperty(propName); + if (Strings.isBlank(propVal)) + throw new IllegalArgumentException(propName + " is not set"); + return propVal; + } + + /** + * Test using a hard-coded authentication token. + * + * <p>This provider does not do authentication, but instead uses a known token for authentication. When Vault is + * initialised, it will give you an <em>Initial Root Token</em>, which can be used for this test. However, + * obviously, passing around a well-known root token is A Bad Idea for use in production, and would largely undo all + * the useful security that vault provides.</p> + * + * <p>Set these system properties:</p> + * + * <p><tt>-Dtest.brooklyn.vault.token=1091fc84-70c1-b266-b99f-781684dd0d2b</tt></p> + */ + @Test(groups = "Live") + public void testHardCodedToken() { + String token = getTestProperty("token"); + ExternalConfigSupplier ecs = new VaultTokenExternalConfigSupplier(null, "test", + ImmutableMap.of("endpoint", endpoint, "token", token, "path", path)); + assertEquals(ecs.get(propertyName), propertyExpectedValue); + } + + /** + * Test using the "userpass" authentication backend. + * + * <p>Configure Vault to enable userpass and add a new user ID with password:</p> + * + * <p><tt>vault auth-enable userpass<br> + * vault write auth/userpass/users/brooklynTest password=s3kr1t policies=root # the "root" policy allows unrestricted access, you will want to use a different policy for real use + * </tt></p> + * + * <p>Set these system properties:</p> + * + * <p><tt>-Dtest.brooklyn.vault.username=brooklynTest<br /> + * -Dtest.brooklyn.vault.password=s3kr1t</tt></p> + */ + @Test(groups = "Live") + public void testUserPassAuthentication() { + String username = getTestProperty("username"); + String password = getTestProperty("password"); + ExternalConfigSupplier ecs = new VaultUserPassExternalConfigSupplier(null, "test", + ImmutableMap.of("endpoint", endpoint, "username", username, "password", password, "path", path)); + assertEquals(ecs.get(propertyName), propertyExpectedValue); + } + + /** + * Test using the "App ID" authentication backend, with a MAC-address based user ID. + * + * <p>First, determine the MAC address of your system. This is system dependent, but a good guess will be to + * inspect the routing table to determine the default route, and take the MAC address of the interface that the + * default route would use. Express the MAC address as a series of 12 low-case hexadecimal digits, without any + * symbols.</p> + * + * <p>Configure Vault to enable App-ID, add a new app ID, and authorise the MAC address to the app ID:</p> + * + * <p><tt>/vault auth-enable app-id<br> + * vault write auth/app-id/map/app-id/brooklyn value=root display_name=Brooklyn # the app ID here is "brooklyn"; the "root" policy allows unrestricted access, you will want to use a different policy for real use + * vault write auth/app-id/map/user-id/0c4de9bca2db value=brooklyn + * </tt></p> + * + * <p>Set these system properties:</p> + * + * <p><tt>-Dtest.brooklyn.vault.appId=brooklyn</tt></p> + */ + @Test(groups = "Live") + public void testAppIdAuthenticationWithAutomaticUserId() { + String appId = getTestProperty("appId"); + ExternalConfigSupplier ecs = new VaultAppIdExternalConfigSupplier(null, "test", + ImmutableMap.of("endpoint", endpoint, "appId", appId, "path", path)); + assertEquals(ecs.get(propertyName), propertyExpectedValue); + } + + /** + * Test using the "App ID" authentication backend, with an explicitly-given user ID. + * + * <p>Configure Vault to enable App-ID, add a new app ID, and authorise your chosen user ID to the app ID:</p> + * + * <p><tt>/vault auth-enable app-id<br> + * vault write auth/app-id/map/app-id/brooklyn value=root display_name=Brooklyn # the app ID here is "brooklyn"; the "root" policy allows unrestricted access, you will want to use a different policy for real use + * vault write auth/app-id/map/user-id/testUser value=brooklyn + * </tt></p> + * + * <p>Set these system properties:</p> + * + * <p><tt>-Dtest.brooklyn.vault.appId=brooklyn<br /> + * -Dtest.brooklyn.vault.userId=testUser</tt></p> + */ + @Test(groups = "Live") + public void testAppIdAuthenticationWithExplicitUserId() { + String appId = getTestProperty("appId"); + String userId = getTestProperty("userId"); + ExternalConfigSupplier ecs = new VaultAppIdExternalConfigSupplier(null, "test", + ImmutableMap.of("endpoint", endpoint, "appId", appId, "path", path, "userId", userId)); + assertEquals(ecs.get(propertyName), propertyExpectedValue); + } + +} \ No newline at end of file
