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

square / keywhiz / 4555816003

pending completion
4555816003

Pull #1205

github

GitHub
Merge c48a5f2b4 into 2bb01dacb
Pull Request #1205: Chloeb/implement undelete

97 of 97 new or added lines in 8 files covered. (100.0%)

5385 of 7159 relevant lines covered (75.22%)

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 keywhiz.api.model.SecretSeries;
39
import okhttp3.Call;
40
import okhttp3.HttpUrl;
41
import okhttp3.MediaType;
42
import okhttp3.OkHttpClient;
43
import okhttp3.Request;
44
import okhttp3.RequestBody;
45
import okhttp3.Response;
46
import org.apache.http.HttpStatus;
47

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

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

60
  private static final String NO_OWNER = null;
×
61

62
  public static class MalformedRequestException extends IOException {
×
63

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

69
  public static class UnauthorizedException extends IOException {
×
70

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

76
  public static class ForbiddenException extends IOException {
×
77

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

83
  public static class NotFoundException extends IOException {
×
84

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

90
  public static class UnsupportedMediaTypeException extends IOException {
×
91

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

97
  public static class ConflictException extends IOException {
×
98

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

104
  public static class ValidationException extends IOException {
×
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

277
  public void undeleteSecret(Long secretId) throws IOException {
278
    httpPost(
×
279
        baseUrl.newBuilder()
×
280
            .addPathSegment("admin")
×
281
            .addPathSegment("secrets")
×
282
            .addPathSegment("undelete")
×
283
            .addPathSegment(secretId.toString())
×
284
            .build(),
×
285
        null
286
    );
287
  }
×
288

289
  public void deleteSecretWithId(long secretId, String mode) {
290
    HttpUrl url = baseUrl.newBuilder()
×
291
        .addPathSegment("admin")
×
292
        .addPathSegment("secrets")
×
293
        .addPathSegment(Long.toString(secretId))
×
294
        .addQueryParameter("mode", mode)
×
295
        .build();
×
296
    httpDeleteQuietly(url);
×
297
  }
×
298

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

305
  public ClientDetailResponse createClient(String name, String description, String spiffeId) throws IOException {
306
    checkArgument(!name.isEmpty());
×
307
    String response = httpPost(baseUrl.resolve("/admin/clients"),
×
308
        CreateClientRequestV2.builder()
×
309
            .name(name)
×
310
            .description(description)
×
311
            .spiffeId(spiffeId)
×
312
            .build());
×
313
    return mapper.readValue(response, ClientDetailResponse.class);
×
314
  }
315

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

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

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

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

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

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

343
  public Client getClientByName(String name) throws IOException {
344
    checkArgument(!name.isEmpty());
×
345
    String response = httpGet(baseUrl.resolve("/admin/clients").newBuilder()
×
346
        .addQueryParameter("name", name)
×
347
        .build());
×
348
    return mapper.readValue(response, Client.class);
×
349
  }
350

351
  public Group getGroupByName(String name) throws IOException {
352
    checkArgument(!name.isEmpty());
×
353
    String response = httpGet(baseUrl.resolve("/admin/groups").newBuilder()
×
354
        .addQueryParameter("name", name)
×
355
        .build());
×
356
    return mapper.readValue(response, Group.class);
×
357
  }
358

359
  public SanitizedSecret getSanitizedSecretByName(String name) throws IOException {
360
    checkArgument(!name.isEmpty());
×
361
    String response =
×
362
        httpGet(baseUrl.resolve("/admin/secrets").newBuilder().addQueryParameter("name", name)
×
363
            .build());
×
364
    return mapper.readValue(response, SanitizedSecret.class);
×
365
  }
366

367
  @Nullable
368
  public List<SecretSeries> getDeletedSecretsByName(String name) throws IOException {
369
    checkArgument(!name.isEmpty());
×
370
    String response =
×
371
        httpGet(baseUrl.newBuilder()
×
372
            .addPathSegment("admin")
×
373
            .addPathSegment("secrets")
×
374
            .addPathSegment("deleted")
×
375
            .addPathSegment(name)
×
376
            .build());
×
377
    return mapper.readValue(response, new TypeReference<>() {
×
378
    });
379
  }
380

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

387
  /**
388
   * Maps some of the common HTTP errors to the corresponding exceptions.
389
   */
390
  private void throwOnCommonError(int status) throws IOException {
391
    switch (status) {
×
392
      case HttpStatus.SC_BAD_REQUEST:
393
        throw new MalformedRequestException();
×
394
      case HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE:
395
        throw new UnsupportedMediaTypeException();
×
396
      case HttpStatus.SC_NOT_FOUND:
397
        throw new NotFoundException();
×
398
      case HttpStatus.SC_UNAUTHORIZED:
399
        throw new UnauthorizedException();
×
400
      case HttpStatus.SC_FORBIDDEN:
401
        throw new ForbiddenException();
×
402
      case HttpStatus.SC_CONFLICT:
403
        throw new ConflictException();
×
404
      case HttpStatus.SC_UNPROCESSABLE_ENTITY:
405
        throw new ValidationException();
×
406
    }
407
    if (status >= 400) {
×
408
      throw new IOException("Unexpected status code on response: " + status);
×
409
    }
410
  }
×
411

412
  private String makeCall(Request request) throws IOException {
413
    Response response = client.newCall(request).execute();
×
414
    try {
415
      throwOnCommonError(response.code());
×
416
    } catch (IOException e) {
×
417
      response.body().close();
×
418
      throw e;
×
419
    }
×
420
    return response.body().string();
×
421
  }
422

423
  private String httpGet(HttpUrl url) throws IOException {
424
    Request request = new Request.Builder()
×
425
        .url(url)
×
426
        .get()
×
427
        .build();
×
428

429
    return makeCall(request);
×
430
  }
431

432
  private String httpPost(HttpUrl url, Object content) throws IOException {
433
    RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(content));
×
434
    Request request = new Request.Builder()
×
435
        .url(url)
×
436
        .post(body)
×
437
        .addHeader(HttpHeaders.CONTENT_TYPE, JSON.toString())
×
438
        .build();
×
439

440
    return makeCall(request);
×
441
  }
442

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

449
    return makeCall(request);
×
450
  }
451

452
  private String httpDelete(HttpUrl url) throws IOException {
453
    Request request = new Request.Builder()
×
454
        .url(url)
×
455
        .delete()
×
456
        .build();
×
457

458
    return makeCall(request);
×
459
  }
460

461
  private String httpDeleteQuietly(HttpUrl url) {
462
    try {
463
      return httpDelete(url);
×
464
    } catch (IOException e) {
×
465
      throw new RuntimeException(e);
×
466
    }
467
  }
468
}
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

© 2025 Coveralls, Inc