com.nike.vault.client.VaultClient.java Source code

Java tutorial

Introduction

Here is the source code for com.nike.vault.client.VaultClient.java

Source

/*
 * Copyright (c) 2016 Nike, Inc.
 *
 * 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 com.nike.vault.client;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.nike.vault.client.auth.VaultCredentialsProvider;
import com.nike.vault.client.http.HttpHeader;
import com.nike.vault.client.http.HttpMethod;
import com.nike.vault.client.http.HttpStatus;
import com.nike.vault.client.model.VaultClientTokenResponse;
import com.nike.vault.client.model.VaultListResponse;
import com.nike.vault.client.model.VaultResponse;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Client for interacting with a Vault.
 */
public class VaultClient {

    public static final String SECRET_PATH_PREFIX = "v1/secret/";

    public static final String AUTH_PATH_PREFIX = "v1/auth/";

    public static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");

    private final VaultCredentialsProvider credentialsProvider;

    private final OkHttpClient httpClient;

    private final UrlResolver urlResolver;

    private final Headers defaultHeaders;

    private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .disableHtmlEscaping().create();

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public VaultClient(final UrlResolver vaultUrlResolver, final VaultCredentialsProvider credentialsProvider,
            final OkHttpClient httpClient, final Headers defaultHeaders) {
        if (vaultUrlResolver == null) {
            throw new IllegalArgumentException("Vault URL resolver cannot be null.");
        }

        if (credentialsProvider == null) {
            throw new IllegalArgumentException("Credentials provider cannot be null.");
        }

        if (httpClient == null) {
            throw new IllegalArgumentException("Http client cannot be null.");
        }

        if (defaultHeaders == null) {
            throw new IllegalArgumentException("Default headers cannot be null.");
        }

        this.urlResolver = vaultUrlResolver;
        this.credentialsProvider = credentialsProvider;
        this.httpClient = httpClient;
        this.defaultHeaders = defaultHeaders;
    }

    /**
     * Explicit constructor that allows for full control over construction of the Vault client.
     *
     * @param vaultUrlResolver    URL resolver for Vault
     * @param credentialsProvider Credential provider for acquiring a token for interacting with Vault
     * @param httpClient          HTTP client for calling Vault
     */
    public VaultClient(final UrlResolver vaultUrlResolver, final VaultCredentialsProvider credentialsProvider,
            final OkHttpClient httpClient) {
        if (vaultUrlResolver == null) {
            throw new IllegalArgumentException("Vault URL resolver can not be null.");
        }

        if (credentialsProvider == null) {
            throw new IllegalArgumentException("Credentials provider can not be null.");
        }

        if (httpClient == null) {
            throw new IllegalArgumentException("Http client can not be null.");
        }

        this.urlResolver = vaultUrlResolver;
        this.credentialsProvider = credentialsProvider;
        this.httpClient = httpClient;
        this.defaultHeaders = new Headers.Builder().build();
    }

    /**
     * List operation for the specified path.  Will return a {@link Map} with a single entry of keys which is an
     * array of strings that represents the keys at that path. If Vault returns an unexpected response code, a
     * {@link VaultServerException} will be thrown with the code and error details.  If an unexpected I/O error is
     * encountered, a {@link VaultClientException} will be thrown wrapping the underlying exception.
     * <p>
     * See https://www.vaultproject.io/docs/secrets/generic/index.html for details on what the list operation returns.
     * </p>
     *
     * @param path Path to the data
     * @return Map containing the keys at that path
     */
    public VaultListResponse list(final String path) {
        final HttpUrl url = buildUrl(SECRET_PATH_PREFIX, path + "?list=true");
        logger.debug("list: requestUrl={}", url);

        final Response response = execute(url, HttpMethod.GET, null);

        if (response.code() == HttpStatus.NOT_FOUND) {
            response.close();
            return new VaultListResponse();
        } else if (response.code() != HttpStatus.OK) {
            parseAndThrowErrorResponse(response);
        }

        final Type mapType = new TypeToken<Map<String, Object>>() {
        }.getType();
        final Map<String, Object> rootData = parseResponseBody(response, mapType);
        return gson.fromJson(gson.toJson(rootData.get("data")), VaultListResponse.class);
    }

    /**
     * Read operation for a specified path.  Will return a {@link Map} of the data stored at the specified path.
     * If Vault returns an unexpected response code, a {@link VaultServerException} will be thrown with the code
     * and error details.  If an unexpected I/O error is encountered, a {@link VaultClientException} will be thrown
     * wrapping the underlying exception.
     *
     * @param path Path to the data
     * @return Map of the data
     */
    public VaultResponse read(final String path) {
        final HttpUrl url = buildUrl(SECRET_PATH_PREFIX, path);
        logger.debug("read: requestUrl={}", url);

        final Response response = execute(url, HttpMethod.GET, null);

        if (response.code() != HttpStatus.OK) {
            parseAndThrowErrorResponse(response);
        }

        return parseResponseBody(response, VaultResponse.class);
    }

    /**
     * Write operation for a specified path and data set. If Vault returns an unexpected response code, a
     * {@link VaultServerException} will be thrown with the code and error details.  If an unexpected I/O
     * error is encountered, a {@link VaultClientException} will be thrown wrapping the underlying exception.
     *
     * @param path Path for where to store the data
     * @param data Data to be stored
     */
    public void write(final String path, final Map<String, String> data) {
        final HttpUrl url = buildUrl(SECRET_PATH_PREFIX, path);
        logger.debug("write: requestUrl={}", url);

        final Response response = execute(url, HttpMethod.POST, data);

        if (response.code() != HttpStatus.NO_CONTENT) {
            parseAndThrowErrorResponse(response);
        }
    }

    /**
     * Delete operation for a specified path.  If Vault returns an unexpected response code, a
     * {@link VaultServerException} will be thrown with the code and error details.  If an unexpected I/O
     * error is encountered, a {@link VaultClientException} will be thrown wrapping the underlying exception.
     *
     * @param path Path to data to be deleted
     */
    public void delete(final String path) {
        final HttpUrl url = buildUrl(SECRET_PATH_PREFIX, path);
        logger.debug("delete: requestUrl={}", url);

        final Response response = execute(url, HttpMethod.DELETE, null);

        if (response.code() != HttpStatus.NO_CONTENT) {
            parseAndThrowErrorResponse(response);
        }
    }

    /**
     * Gets all the details about the client token being used by the requester.  Also serves as a simple way
     * to test that a token is still active.  If an unexpected response is recieved, a {@link VaultServerException}
     * will be thrown with details.
     *
     * @return Client token details
     */
    public VaultClientTokenResponse lookupSelf() {
        final HttpUrl url = buildUrl(AUTH_PATH_PREFIX, "token/lookup-self");
        logger.debug("lookupSelf: requestUrl={}", url);

        final Response response = execute(url, HttpMethod.GET, null);

        if (response.code() != HttpStatus.OK) {
            parseAndThrowErrorResponse(response);
        }

        final Type mapType = new TypeToken<Map<String, Object>>() {
        }.getType();
        final Map<String, Object> rootData = parseResponseBody(response, mapType);
        return gson.fromJson(gson.toJson(rootData.get("data")), VaultClientTokenResponse.class);
    }

    /**
     * Returns a copy of the URL being used for communicating with Vault
     *
     * @return Copy of the HttpUrl object
     */
    public HttpUrl getVaultUrl() {
        return HttpUrl.parse(urlResolver.resolve());
    }

    /**
     * Returns the configured credentials provider.
     *
     * @return The configured credentials provider
     */
    public VaultCredentialsProvider getCredentialsProvider() {
        return credentialsProvider;
    }

    /**
     * Gets the Gson object used for serializing and de-serializing requests.
     *
     * @return Gson object
     */
    public Gson getGson() {
        return gson;
    }

    /**
     * Returns the configured default HTTP headers.
     *
     * @return The configured default HTTP headers
     */
    public Headers getDefaultHeaders() {
        return defaultHeaders;
    }

    /**
     * Builds the full URL for preforming an operation against Vault.
     *
     * @param prefix Prefix between the environment URL and specified path
     * @param path   Path for the requested operation
     * @return Full URL to execute a request against
     */
    protected HttpUrl buildUrl(final String prefix, final String path) {
        String baseUrl = urlResolver.resolve();

        if (!StringUtils.endsWith(baseUrl, "/")) {
            baseUrl += "/";
        }

        return HttpUrl.parse(baseUrl + prefix + path);
    }

    /**
     * Executes the HTTP request based on the input parameters.
     *
     * @param url         The URL to execute the request against
     * @param method      The HTTP method for the request
     * @param requestBody The request body of the HTTP request
     * @return Response from the server
     */
    protected Response execute(final HttpUrl url, final String method, final Object requestBody) {
        try {
            Request request = buildRequest(url, method, requestBody);

            return httpClient.newCall(request).execute();
        } catch (IOException e) {
            if (e instanceof SSLException && e.getMessage() != null
                    && e.getMessage().contains("Unrecognized SSL message, plaintext connection?")) {
                // AnyConnect web security proxy can be disabled with:
                //  `sudo /opt/cisco/anyconnect/bin/acwebsecagent -disablesvc -websecurity`
                throw new VaultClientException(
                        "I/O error while communicating with vault. Unrecognized SSL message may be due to a web proxy e.g. AnyConnect",
                        e);
            } else {
                throw new VaultClientException("I/O error while communicating with vault.", e);
            }
        }
    }

    /**
     * Build the HTTP request to execute for the Vault Client
     * @param url         The URL to execute the request against
     * @param method      The HTTP method for the request
     * @param requestBody The request body of the HTTP request
     * @return - The HTTP request
     */
    protected Request buildRequest(final HttpUrl url, final String method, final Object requestBody) {
        Request.Builder requestBuilder = new Request.Builder().url(url).headers(defaultHeaders) // call headers method first because it overwrites all existing headers
                .addHeader(HttpHeader.VAULT_TOKEN, credentialsProvider.getCredentials().getToken())
                .addHeader(HttpHeader.ACCEPT, DEFAULT_MEDIA_TYPE.toString());

        if (requestBody != null) {
            requestBuilder.addHeader(HttpHeader.CONTENT_TYPE, DEFAULT_MEDIA_TYPE.toString()).method(method,
                    RequestBody.create(DEFAULT_MEDIA_TYPE, gson.toJson(requestBody)));
        } else {
            requestBuilder.method(method, null);
        }

        return requestBuilder.build();
    }

    /**
     * Convenience method for parsing the HTTP response and mapping it to a class.
     *
     * @param response      The HTTP response object
     * @param responseClass The class to map the response body to
     * @param <M>           Represents the type to map to
     * @return Deserialized object from the response body
     */
    protected <M> M parseResponseBody(final Response response, final Class<M> responseClass) {
        final String responseBodyStr = responseBodyAsString(response);
        try {
            return gson.fromJson(responseBodyStr, responseClass);
        } catch (JsonSyntaxException e) {
            logger.error("parseResponseBody: responseCode={}, requestUrl={}, response={}", response.code(),
                    response.request().url(), responseBodyStr);
            throw new VaultClientException(
                    "Error parsing the response body from vault, response code: " + response.code(), e);
        }
    }

    /**
     * Convenience method for parsing the HTTP response and mapping it to a type.
     *
     * @param response The HTTP response object
     * @param typeOf   The type to map the response body to
     * @param <M>      Represents the type to map to
     * @return Deserialized object from the response body
     */
    protected <M> M parseResponseBody(final Response response, final Type typeOf) {
        final String responseBodyStr = responseBodyAsString(response);
        try {
            return gson.fromJson(responseBodyStr, typeOf);
        } catch (JsonSyntaxException e) {
            logger.error("parseResponseBody: responseCode={}, requestUrl={}, response={}", response.code(),
                    response.request().url(), responseBodyStr);
            throw new VaultClientException(
                    "Error parsing the response body from vault, response code: " + response.code(), e);
        }
    }

    /**
     * Convenience method for parsing the errors from the HTTP response and throwing a {@link VaultServerException}.
     *
     * @param response Response to parses the error details from
     */
    protected void parseAndThrowErrorResponse(final Response response) {
        final String responseBodyStr = responseBodyAsString(response);
        logger.debug("parseAndThrowErrorResponse: responseCode={}, requestUrl={}, response={}", response.code(),
                response.request().url(), responseBodyStr);

        try {
            ErrorResponse errorResponse = gson.fromJson(responseBodyStr, ErrorResponse.class);

            if (errorResponse != null) {
                throw new VaultServerException(response.code(), errorResponse.getErrors());
            } else {
                throw new VaultServerException(response.code(), new LinkedList<String>());
            }
        } catch (JsonSyntaxException e) {
            logger.error("ERROR Failed to parse error message, response body received: {}", responseBodyStr);
            throw new VaultClientException(
                    "Error parsing the error response body from vault, response code: " + response.code(), e);
        }
    }

    /**
     * POJO for representing error response body from Vault.
     */
    protected static class ErrorResponse {
        private List<String> errors;

        public List<String> getErrors() {
            return errors;
        }
    }

    protected String responseBodyAsString(Response response) {
        try {
            return response.body().string();
        } catch (IOException ioe) {
            logger.debug("responseBodyAsString: response={}", gson.toJson(response));
            return "ERROR failed to print response body as str: " + ioe.getMessage();
        }
    }
}