You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/11/05 18:29:43 UTC

[4/9] incubator-brooklyn git commit: Add Vault external configuration supplier

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 <ri...@apache.org>
Authored: Thu Nov 5 16:07:54 2015 +0000
Committer: Richard Downer <ri...@apache.org>
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