keywhiz.client.KeywhizClient.java Source code

Java tutorial

Introduction

Here is the source code for keywhiz.client.KeywhizClient.java

Source

/*
 * Copyright (C) 2015 Square, 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 keywhiz.client;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import keywhiz.api.ClientDetailResponse;
import keywhiz.api.CreateClientRequest;
import keywhiz.api.CreateGroupRequest;
import keywhiz.api.CreateSecretRequest;
import keywhiz.api.GroupDetailResponse;
import keywhiz.api.LoginRequest;
import keywhiz.api.SecretDetailResponse;
import keywhiz.api.automation.v2.CreateOrUpdateSecretRequestV2;
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
import keywhiz.api.automation.v2.SecretDetailResponseV2;
import keywhiz.api.model.Client;
import keywhiz.api.model.Group;
import keywhiz.api.model.SanitizedSecret;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.http.HttpStatus;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;

/**
 * Client for interacting with the Keywhiz Server.
 *
 * Facilitates the manipulation of Clients, Groups, Secrets and the connections between them.
 */
public class KeywhizClient {
    public static final MediaType JSON = MediaType.parse("application/json");

    public static class MalformedRequestException extends IOException {

        @Override
        public String getMessage() {
            return "Malformed request syntax from client (400)";
        }
    }

    public static class UnauthorizedException extends IOException {

        @Override
        public String getMessage() {
            return "Not allowed to login, password may be incorrect (401)";
        }
    }

    public static class ForbiddenException extends IOException {

        @Override
        public String getMessage() {
            return "Resource forbidden (403)";
        }
    }

    public static class NotFoundException extends IOException {

        @Override
        public String getMessage() {
            return "Resource not found (404)";
        }
    }

    public static class UnsupportedMediaTypeException extends IOException {

        @Override
        public String getMessage() {
            return "Resource media type is incorrect or incompatible (415)";
        }
    }

    public static class ConflictException extends IOException {

        @Override
        public String getMessage() {
            return "Conflicting resource (409)";
        }
    }

    public static class ValidationException extends IOException {

        @Override
        public String getMessage() {
            return "Malformed request semantics from client (422)";
        }
    }

    private final ObjectMapper mapper;
    private final OkHttpClient client;
    private final HttpUrl baseUrl;

    public KeywhizClient(ObjectMapper mapper, OkHttpClient client, HttpUrl baseUrl) {
        this.mapper = checkNotNull(mapper);
        this.client = checkNotNull(client);
        this.baseUrl = checkNotNull(baseUrl);
    }

    /**
     * Login to the Keywhiz server.
     *
     * Future requests made using this client instance will be authenticated.
     * @param username login username
     * @param password login password
     * @throws IOException if a network IO error occurs
     */
    public void login(String username, char[] password) throws IOException {
        httpPost(baseUrl.resolve("/admin/login"), LoginRequest.from(username, password));
    }

    public List<Group> allGroups() throws IOException {
        String response = httpGet(baseUrl.resolve("/admin/groups/"));
        return mapper.readValue(response, new TypeReference<List<Group>>() {
        });
    }

    public GroupDetailResponse createGroup(String name, String description, ImmutableMap<String, String> metadata)
            throws IOException {
        checkArgument(!name.isEmpty());
        String response = httpPost(baseUrl.resolve("/admin/groups"),
                new CreateGroupRequest(name, description, metadata));
        return mapper.readValue(response, GroupDetailResponse.class);
    }

    public GroupDetailResponse groupDetailsForId(long groupId) throws IOException {
        String response = httpGet(baseUrl.resolve(format("/admin/groups/%d", groupId)));
        return mapper.readValue(response, GroupDetailResponse.class);
    }

    public void deleteGroupWithId(long groupId) throws IOException {
        httpDelete(baseUrl.resolve(format("/admin/groups/%d", groupId)));
    }

    public List<SanitizedSecret> allSecrets() throws IOException {
        String response = httpGet(baseUrl.resolve("/admin/secrets?nameOnly=1"));
        return mapper.readValue(response, new TypeReference<List<SanitizedSecret>>() {
        });
    }

    public List<SanitizedSecret> allSecretsBatched(int idx, int num, boolean newestFirst) throws IOException {
        String response = httpGet(baseUrl
                .resolve(String.format("/admin/secrets?idx=%d&num=%d&newestFirst=%s", idx, num, newestFirst)));
        return mapper.readValue(response, new TypeReference<List<SanitizedSecret>>() {
        });
    }

    public SecretDetailResponse createSecret(String name, String description, byte[] content,
            ImmutableMap<String, String> metadata, long expiry) throws IOException {
        checkArgument(!name.isEmpty());
        checkArgument(content.length > 0, "Content must not be empty");

        String b64Content = Base64.getEncoder().encodeToString(content);
        CreateSecretRequest request = new CreateSecretRequest(name, description, b64Content, metadata, expiry);
        String response = httpPost(baseUrl.resolve("/admin/secrets"), request);
        return mapper.readValue(response, SecretDetailResponse.class);
    }

    public SecretDetailResponse updateSecret(String name, boolean descriptionPresent, String description,
            boolean contentPresent, byte[] content, boolean metadataPresent, ImmutableMap<String, String> metadata,
            boolean expiryPresent, long expiry) throws IOException {
        checkArgument(!name.isEmpty());

        String b64Content = Base64.getEncoder().encodeToString(content);
        PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder()
                .descriptionPresent(descriptionPresent).description(description).contentPresent(contentPresent)
                .content(b64Content).metadataPresent(metadataPresent).metadata(metadata)
                .expiryPresent(expiryPresent).expiry(expiry).build();
        String response = httpPost(baseUrl.resolve(format("/admin/secrets/%s/partialupdate", name)), request);
        return mapper.readValue(response, SecretDetailResponse.class);
    }

    public SecretDetailResponse secretDetailsForId(long secretId) throws IOException {
        String response = httpGet(baseUrl.resolve(format("/admin/secrets/%d", secretId)));
        return mapper.readValue(response, SecretDetailResponse.class);
    }

    public List<SanitizedSecret> listSecretVersions(String name, int idx, int numVersions) throws IOException {
        String response = httpGet(baseUrl.resolve(
                format("/admin/secrets/versions/%s?versionIdx=%d&numVersions=%d", name, idx, numVersions)));
        return mapper.readValue(response, new TypeReference<List<SanitizedSecret>>() {
        });
    }

    public SecretDetailResponse rollbackSecret(String name, long version) throws IOException {
        String response = httpPost(baseUrl.resolve(format("/admin/secrets/rollback/%s/%d", name, version)), null);
        return mapper.readValue(response, SecretDetailResponse.class);
    }

    public void deleteSecretWithId(long secretId) throws IOException {
        httpDelete(baseUrl.resolve(format("/admin/secrets/%d", secretId)));
    }

    public List<Client> allClients() throws IOException {
        String httpResponse = httpGet(baseUrl.resolve("/admin/clients/"));
        return mapper.readValue(httpResponse, new TypeReference<List<Client>>() {
        });
    }

    public ClientDetailResponse createClient(String name) throws IOException {
        checkArgument(!name.isEmpty());
        String response = httpPost(baseUrl.resolve("/admin/clients"), new CreateClientRequest(name));
        return mapper.readValue(response, ClientDetailResponse.class);
    }

    public ClientDetailResponse clientDetailsForId(long clientId) throws IOException {
        String response = httpGet(baseUrl.resolve(format("/admin/clients/%d", clientId)));
        return mapper.readValue(response, ClientDetailResponse.class);
    }

    public void deleteClientWithId(long clientId) throws IOException {
        httpDelete(baseUrl.resolve(format("/admin/clients/%d", clientId)));
    }

    public void enrollClientInGroupByIds(long clientId, long groupId) throws IOException {
        httpPut(baseUrl.resolve(format("/admin/memberships/clients/%d/groups/%d", clientId, groupId)));
    }

    public void evictClientFromGroupByIds(long clientId, long groupId) throws IOException {
        httpDelete(baseUrl.resolve(format("/admin/memberships/clients/%d/groups/%d", clientId, groupId)));
    }

    public void grantSecretToGroupByIds(long secretId, long groupId) throws IOException {
        httpPut(baseUrl.resolve(format("/admin/memberships/secrets/%d/groups/%d", secretId, groupId)));
    }

    public void revokeSecretFromGroupByIds(long secretId, long groupId) throws IOException {
        httpDelete(baseUrl.resolve(format("/admin/memberships/secrets/%d/groups/%d", secretId, groupId)));
    }

    public Client getClientByName(String name) throws IOException {
        checkArgument(!name.isEmpty());
        String response = httpGet(
                baseUrl.resolve("/admin/clients").newBuilder().addQueryParameter("name", name).build());
        return mapper.readValue(response, Client.class);
    }

    public Group getGroupByName(String name) throws IOException {
        checkArgument(!name.isEmpty());
        String response = httpGet(
                baseUrl.resolve("/admin/groups").newBuilder().addQueryParameter("name", name).build());
        return mapper.readValue(response, Group.class);
    }

    public SanitizedSecret getSanitizedSecretByName(String name) throws IOException {
        checkArgument(!name.isEmpty());
        String response = httpGet(
                baseUrl.resolve("/admin/secrets").newBuilder().addQueryParameter("name", name).build());
        return mapper.readValue(response, SanitizedSecret.class);
    }

    public boolean isLoggedIn() throws IOException {
        HttpUrl url = baseUrl.resolve("/admin/me");
        Call call = client.newCall(new Request.Builder().get().url(url).build());
        return call.execute().code() != HttpStatus.SC_UNAUTHORIZED;
    }

    /**
     * Maps some of the common HTTP errors to the corresponding exceptions.
     */
    private void throwOnCommonError(int status) throws IOException {
        switch (status) {
        case HttpStatus.SC_BAD_REQUEST:
            throw new MalformedRequestException();
        case HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE:
            throw new UnsupportedMediaTypeException();
        case HttpStatus.SC_NOT_FOUND:
            throw new NotFoundException();
        case HttpStatus.SC_UNAUTHORIZED:
            throw new UnauthorizedException();
        case HttpStatus.SC_FORBIDDEN:
            throw new ForbiddenException();
        case HttpStatus.SC_CONFLICT:
            throw new ConflictException();
        case HttpStatus.SC_UNPROCESSABLE_ENTITY:
            throw new ValidationException();
        }
        if (status >= 400) {
            throw new IOException("Unexpected status code on response: " + status);
        }
    }

    private String makeCall(Request request) throws IOException {
        Response response = client.newCall(request).execute();
        try {
            throwOnCommonError(response.code());
        } catch (IOException e) {
            response.body().close();
            throw e;
        }
        return response.body().string();
    }

    private String httpGet(HttpUrl url) throws IOException {
        Request request = new Request.Builder().url(url).get().build();

        return makeCall(request);
    }

    private String httpPost(HttpUrl url, Object content) throws IOException {
        RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(content));
        Request request = new Request.Builder().url(url).post(body)
                .addHeader(HttpHeaders.CONTENT_TYPE, JSON.toString()).build();

        return makeCall(request);
    }

    private String httpPut(HttpUrl url) throws IOException {
        Request request = new Request.Builder().url(url).put(RequestBody.create(MediaType.parse("text/plain"), ""))
                .build();

        return makeCall(request);
    }

    private String httpDelete(HttpUrl url) throws IOException {
        Request request = new Request.Builder().url(url).delete().build();

        return makeCall(request);
    }
}