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

square / keywhiz / 3998983595

pending completion
3998983595

push

github

GitHub
Merge pull request #1189 from square/chloeb/datasec-677

Adding CLI option to the "describe secrets" action to include deleted secrets

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

5237 of 6968 relevant lines covered (75.16%)

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.annotation.Nullable;
26
import javax.ws.rs.core.HttpHeaders;
27
import keywhiz.api.ClientDetailResponse;
28
import keywhiz.api.GroupDetailResponse;
29
import keywhiz.api.LoginRequest;
30
import keywhiz.api.SecretDetailResponse;
31
import keywhiz.api.automation.v2.CreateClientRequestV2;
32
import keywhiz.api.automation.v2.CreateGroupRequestV2;
33
import keywhiz.api.automation.v2.CreateSecretRequestV2;
34
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
35
import keywhiz.api.model.Client;
36
import keywhiz.api.model.Group;
37
import keywhiz.api.model.SanitizedSecret;
38
import okhttp3.Call;
39
import okhttp3.HttpUrl;
40
import okhttp3.MediaType;
41
import okhttp3.OkHttpClient;
42
import okhttp3.Request;
43
import okhttp3.RequestBody;
44
import okhttp3.Response;
45
import org.apache.http.HttpStatus;
46

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

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

59
  private static final String NO_OWNER = null;
×
60

61
  public static class MalformedRequestException extends IOException {
×
62

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

68
  public static class UnauthorizedException extends IOException {
×
69

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

75
  public static class ForbiddenException extends IOException {
×
76

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

82
  public static class NotFoundException extends IOException {
×
83

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

89
  public static class UnsupportedMediaTypeException extends IOException {
×
90

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

96
  public static class ConflictException extends IOException {
×
97

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

103
  public static class ValidationException extends IOException {
×
104

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

354
  @Nullable
355
  public List<SanitizedSecret> getDeletedSecretsByName(String name) throws IOException {
356
    checkArgument(!name.isEmpty());
×
357
    String response =
×
358
        httpGet(baseUrl.newBuilder()
×
359
            .addPathSegment("admin")
×
360
            .addPathSegment("secrets")
×
361
            .addPathSegment("deleted")
×
362
            .addPathSegment(name)
×
363
            .build());
×
364
    return mapper.readValue(response, new TypeReference<>() {
×
365
    });
366
  }
367

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

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

399
  private String makeCall(Request request) throws IOException {
400
    Response response = client.newCall(request).execute();
×
401
    try {
402
      throwOnCommonError(response.code());
×
403
    } catch (IOException e) {
×
404
      response.body().close();
×
405
      throw e;
×
406
    }
×
407
    return response.body().string();
×
408
  }
409

410
  private String httpGet(HttpUrl url) throws IOException {
411
    Request request = new Request.Builder()
×
412
        .url(url)
×
413
        .get()
×
414
        .build();
×
415

416
    return makeCall(request);
×
417
  }
418

419
  private String httpPost(HttpUrl url, Object content) throws IOException {
420
    RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(content));
×
421
    Request request = new Request.Builder()
×
422
        .url(url)
×
423
        .post(body)
×
424
        .addHeader(HttpHeaders.CONTENT_TYPE, JSON.toString())
×
425
        .build();
×
426

427
    return makeCall(request);
×
428
  }
429

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

436
    return makeCall(request);
×
437
  }
438

439
  private String httpDelete(HttpUrl url) throws IOException {
440
    Request request = new Request.Builder()
×
441
        .url(url)
×
442
        .delete()
×
443
        .build();
×
444

445
    return makeCall(request);
×
446
  }
447

448
  private String httpDeleteQuietly(HttpUrl url) {
449
    try {
450
      return httpDelete(url);
×
451
    } catch (IOException e) {
×
452
      throw new RuntimeException(e);
×
453
    }
454
  }
455
}
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