• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

square / keywhiz / 3963561994

pending completion
3963561994

Pull #1189

github

GitHub
Merge 0e51f2a1d into c1e040c06
Pull Request #1189: Adding CLI option to the "describe secrets" action to include deleted secrets

33 of 33 new or added lines in 4 files covered. (100.0%)

5238 of 6959 relevant lines covered (75.27%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/client/src/main/java/keywhiz/client/KeywhizClient.java
1
/*
2
 * Copyright (C) 2015 Square, Inc.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package keywhiz.client;
18

19
import com.fasterxml.jackson.core.type.TypeReference;
20
import com.fasterxml.jackson.databind.ObjectMapper;
21
import com.google.common.collect.ImmutableMap;
22
import java.io.IOException;
23
import java.util.Base64;
24
import java.util.List;
25
import javax.ws.rs.core.HttpHeaders;
26
import keywhiz.api.ClientDetailResponse;
27
import keywhiz.api.GroupDetailResponse;
28
import keywhiz.api.LoginRequest;
29
import keywhiz.api.SecretDetailResponse;
30
import keywhiz.api.automation.v2.CreateClientRequestV2;
31
import keywhiz.api.automation.v2.CreateGroupRequestV2;
32
import keywhiz.api.automation.v2.CreateSecretRequestV2;
33
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
34
import keywhiz.api.model.Client;
35
import keywhiz.api.model.Group;
36
import keywhiz.api.model.SanitizedSecret;
37
import okhttp3.Call;
38
import okhttp3.HttpUrl;
39
import okhttp3.MediaType;
40
import okhttp3.OkHttpClient;
41
import okhttp3.Request;
42
import okhttp3.RequestBody;
43
import okhttp3.Response;
44
import org.apache.http.HttpStatus;
45

46
import static com.google.common.base.Preconditions.checkArgument;
47
import static com.google.common.base.Preconditions.checkNotNull;
48
import static java.lang.String.format;
49

50
/**
51
 * Client for interacting with the Keywhiz Server.
52
 * <p>
53
 * Facilitates the manipulation of Clients, Groups, Secrets and the connections between them.
54
 */
55
public class KeywhizClient {
56
  public static final MediaType JSON = MediaType.parse("application/json");
×
57

58
  private static final String NO_OWNER = null;
×
59

60
  public static class MalformedRequestException extends IOException {
×
61

62
    @Override public String getMessage() {
63
      return "Malformed request syntax from client (400)";
×
64
    }
65
  }
66

67
  public static class UnauthorizedException extends IOException {
×
68

69
    @Override public String getMessage() {
70
      return "Not allowed to login, password may be incorrect (401)";
×
71
    }
72
  }
73

74
  public static class ForbiddenException extends IOException {
×
75

76
    @Override public String getMessage() {
77
      return "Resource forbidden (403)";
×
78
    }
79
  }
80

81
  public static class NotFoundException extends IOException {
×
82

83
    @Override public String getMessage() {
84
      return "Resource not found (404)";
×
85
    }
86
  }
87

88
  public static class UnsupportedMediaTypeException extends IOException {
×
89

90
    @Override public String getMessage() {
91
      return "Resource media type is incorrect or incompatible (415)";
×
92
    }
93
  }
94

95
  public static class ConflictException extends IOException {
×
96

97
    @Override public String getMessage() {
98
      return "Conflicting resource (409)";
×
99
    }
100
  }
101

102
  public static class ValidationException extends IOException {
×
103

104
    @Override public String getMessage() {
105
      return "Malformed request semantics from client (422)";
×
106
    }
107
  }
108

109
  private final ObjectMapper mapper;
110
  private final OkHttpClient client;
111
  private final HttpUrl baseUrl;
112

113
  public KeywhizClient(ObjectMapper mapper, OkHttpClient client, HttpUrl baseUrl) {
×
114
    this.mapper = checkNotNull(mapper);
×
115
    this.client = checkNotNull(client);
×
116
    this.baseUrl = checkNotNull(baseUrl);
×
117
  }
×
118

119
  /**
120
   * Login to the Keywhiz server.
121
   * <p>
122
   * Future requests made using this client instance will be authenticated.
123
   *
124
   * @param username login username
125
   * @param password login password
126
   * @throws IOException if a network IO error occurs
127
   */
128
  public void login(String username, char[] password) throws IOException {
129
    httpPost(baseUrl.resolve("/admin/login"), LoginRequest.from(username, password));
×
130
  }
×
131

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

138
  public GroupDetailResponse createGroup(String name, String description,
139
      ImmutableMap<String, String> metadata) throws IOException {
140
    checkArgument(!name.isEmpty());
×
141
    String response = httpPost(baseUrl.resolve("/admin/groups"),
×
142
        CreateGroupRequestV2.builder()
×
143
            .name(name)
×
144
            .description(description)
×
145
            .metadata(metadata)
×
146
            .build());
×
147
    return mapper.readValue(response, GroupDetailResponse.class);
×
148
  }
149

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

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

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

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

173
  public SecretDetailResponse createSecret(
174
      String name,
175
      String description,
176
      byte[] content,
177
      ImmutableMap<String, String> metadata,
178
      long expiry) throws IOException {
179

180
    return createSecret(
×
181
        name,
182
        NO_OWNER,
183
        description,
184
        content,
185
        metadata,
186
        expiry
187
    );
188
  }
189

190
  public SecretDetailResponse createSecret(
191
      String name,
192
      String owner,
193
      String description,
194
      byte[] content,
195
      ImmutableMap<String, String> metadata,
196
      long expiry) throws IOException {
197

198
    checkArgument(!name.isEmpty());
×
199
    checkArgument(content.length > 0, "Content must not be empty");
×
200

201
    String b64Content = Base64.getEncoder().encodeToString(content);
×
202
    CreateSecretRequestV2 request =
203
        CreateSecretRequestV2.builder()
×
204
            .name(name)
×
205
            .owner(owner)
×
206
            .description(description)
×
207
            .content(b64Content)
×
208
            .metadata(metadata)
×
209
            .expiry(expiry)
×
210
            .build();
×
211
    String response = httpPost(baseUrl.resolve("/admin/secrets"), request);
×
212
    return mapper.readValue(response, SecretDetailResponse.class);
×
213
  }
214

215
  public SecretDetailResponse partialUpdateSecret(
216
      String secretName,
217
      PartialUpdateSecretRequestV2 request) throws IOException {
218
    HttpUrl url = baseUrl.resolve(
×
219
        format(
×
220
            "/admin/secrets/%s/partialupdate",
221
            secretName));
222
    String response = httpPost(url, request);
×
223
    return mapper.readValue(response, SecretDetailResponse.class);
×
224
  }
225

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

232
    String b64Content = Base64.getEncoder().encodeToString(content);
×
233
    PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder()
×
234
        .descriptionPresent(descriptionPresent)
×
235
        .description(description)
×
236
        .contentPresent(contentPresent)
×
237
        .content(b64Content)
×
238
        .metadataPresent(metadataPresent)
×
239
        .metadata(metadata)
×
240
        .expiryPresent(expiryPresent)
×
241
        .expiry(expiry)
×
242
        .build();
×
243
    String response =
×
244
        httpPost(baseUrl.resolve(format("/admin/secrets/%s/partialupdate", name)), request);
×
245
    return mapper.readValue(response, SecretDetailResponse.class);
×
246
  }
247

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

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

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

267
  public void renameSecret(long secretId, String newName) throws IOException {
268
    httpPost(baseUrl.resolve(format("/admin/secrets/rename/%d/%s", secretId, newName)), null);
×
269
  }
×
270

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

275
  public void deleteSecretWithId(long secretId, String mode) {
276
    HttpUrl url = baseUrl.newBuilder()
×
277
        .addPathSegment("admin")
×
278
        .addPathSegment("secrets")
×
279
        .addPathSegment(Long.toString(secretId))
×
280
        .addQueryParameter("mode", mode)
×
281
        .build();
×
282
    httpDeleteQuietly(url);
×
283
  }
×
284

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

291
  public ClientDetailResponse createClient(String name, String description, String spiffeId) throws IOException {
292
    checkArgument(!name.isEmpty());
×
293
    String response = httpPost(baseUrl.resolve("/admin/clients"),
×
294
        CreateClientRequestV2.builder()
×
295
            .name(name)
×
296
            .description(description)
×
297
            .spiffeId(spiffeId)
×
298
            .build());
×
299
    return mapper.readValue(response, ClientDetailResponse.class);
×
300
  }
301

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

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

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

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

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

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

329
  public Client getClientByName(String name) throws IOException {
330
    checkArgument(!name.isEmpty());
×
331
    String response = httpGet(baseUrl.resolve("/admin/clients").newBuilder()
×
332
        .addQueryParameter("name", name)
×
333
        .build());
×
334
    return mapper.readValue(response, Client.class);
×
335
  }
336

337
  public Group getGroupByName(String name) throws IOException {
338
    checkArgument(!name.isEmpty());
×
339
    String response = httpGet(baseUrl.resolve("/admin/groups").newBuilder()
×
340
        .addQueryParameter("name", name)
×
341
        .build());
×
342
    return mapper.readValue(response, Group.class);
×
343
  }
344

345
  public SanitizedSecret getSanitizedSecretByName(String name) throws IOException {
346
    checkArgument(!name.isEmpty());
×
347
    String response =
×
348
        httpGet(baseUrl.resolve("/admin/secrets").newBuilder().addQueryParameter("name", name)
×
349
            .build());
×
350
    return mapper.readValue(response, SanitizedSecret.class);
×
351
  }
352

353
  public List<SanitizedSecret> getDeletedSecretsByName(String name) throws IOException {
354
    checkArgument(!name.isEmpty());
×
355
    String response =
×
356
        httpGet(baseUrl.resolve(format("/admin/secrets/deleted/%s", name)).newBuilder().build());
×
357
    return mapper.readValue(response, new TypeReference<>() {
×
358
    });
359
  }
360

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

367
  /**
368
   * Maps some of the common HTTP errors to the corresponding exceptions.
369
   */
370
  private void throwOnCommonError(int status) throws IOException {
371
    switch (status) {
×
372
      case HttpStatus.SC_BAD_REQUEST:
373
        throw new MalformedRequestException();
×
374
      case HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE:
375
        throw new UnsupportedMediaTypeException();
×
376
      case HttpStatus.SC_NOT_FOUND:
377
        throw new NotFoundException();
×
378
      case HttpStatus.SC_UNAUTHORIZED:
379
        throw new UnauthorizedException();
×
380
      case HttpStatus.SC_FORBIDDEN:
381
        throw new ForbiddenException();
×
382
      case HttpStatus.SC_CONFLICT:
383
        throw new ConflictException();
×
384
      case HttpStatus.SC_UNPROCESSABLE_ENTITY:
385
        throw new ValidationException();
×
386
    }
387
    if (status >= 400) {
×
388
      throw new IOException("Unexpected status code on response: " + status);
×
389
    }
390
  }
×
391

392
  private String makeCall(Request request) throws IOException {
393
    Response response = client.newCall(request).execute();
×
394
    try {
395
      throwOnCommonError(response.code());
×
396
    } catch (IOException e) {
×
397
      response.body().close();
×
398
      throw e;
×
399
    }
×
400
    return response.body().string();
×
401
  }
402

403
  private String httpGet(HttpUrl url) throws IOException {
404
    Request request = new Request.Builder()
×
405
        .url(url)
×
406
        .get()
×
407
        .build();
×
408

409
    return makeCall(request);
×
410
  }
411

412
  private String httpPost(HttpUrl url, Object content) throws IOException {
413
    RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(content));
×
414
    Request request = new Request.Builder()
×
415
        .url(url)
×
416
        .post(body)
×
417
        .addHeader(HttpHeaders.CONTENT_TYPE, JSON.toString())
×
418
        .build();
×
419

420
    return makeCall(request);
×
421
  }
422

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

429
    return makeCall(request);
×
430
  }
431

432
  private String httpDelete(HttpUrl url) throws IOException {
433
    Request request = new Request.Builder()
×
434
        .url(url)
×
435
        .delete()
×
436
        .build();
×
437

438
    return makeCall(request);
×
439
  }
440

441
  private String httpDeleteQuietly(HttpUrl url) {
442
    try {
443
      return httpDelete(url);
×
444
    } catch (IOException e) {
×
445
      throw new RuntimeException(e);
×
446
    }
447
  }
448
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc