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

square / keywhiz / 4557915550

pending completion
4557915550

Pull #1205

github

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

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

5390 of 7169 relevant lines covered (75.18%)

0.75 hits per line

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

94.57
/server/src/main/java/keywhiz/service/daos/SecretDAO.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.service.daos;
18

19
import com.google.common.annotations.VisibleForTesting;
20
import com.google.common.collect.ImmutableList;
21
import com.google.common.collect.ImmutableMap;
22
import com.google.common.collect.Lists;
23
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
24
import keywhiz.api.model.Group;
25
import keywhiz.api.model.SanitizedSecret;
26
import keywhiz.api.model.Secret;
27
import keywhiz.api.model.SecretContent;
28
import keywhiz.api.model.SecretSeries;
29
import keywhiz.api.model.SecretSeriesAndContent;
30
import keywhiz.jooq.tables.Secrets;
31
import keywhiz.service.config.Readonly;
32
import keywhiz.service.crypto.ContentCryptographer;
33
import keywhiz.service.crypto.ContentEncodingException;
34
import keywhiz.service.daos.SecretContentDAO.SecretContentDAOFactory;
35
import keywhiz.service.daos.SecretSeriesDAO.SecretSeriesDAOFactory;
36
import keywhiz.service.exceptions.ConflictException;
37
import keywhiz.service.permissions.PermissionCheck;
38
import org.joda.time.DateTime;
39
import org.jooq.Configuration;
40
import org.jooq.DSLContext;
41
import org.jooq.exception.DataAccessException;
42
import org.jooq.impl.DSL;
43

44
import javax.annotation.Nullable;
45
import javax.inject.Inject;
46
import javax.ws.rs.BadRequestException;
47
import javax.ws.rs.NotFoundException;
48
import java.time.Instant;
49
import java.time.OffsetDateTime;
50
import java.util.AbstractMap.SimpleEntry;
51
import java.util.ArrayList;
52
import java.util.List;
53
import java.util.Map;
54
import java.util.Optional;
55

56
import static com.google.common.base.Preconditions.checkArgument;
57
import static com.google.common.base.Preconditions.checkNotNull;
58
import static java.lang.String.format;
59
import static java.nio.charset.StandardCharsets.UTF_8;
60
import static java.util.stream.Collectors.toList;
61
import static keywhiz.jooq.tables.Secrets.SECRETS;
62

63
/**
64
 * Primary class to interact with {@link Secret}s.
65
 *
66
 * Does not map to a table itself, but utilizes both {@link SecretSeriesDAO} and {@link
67
 * SecretContentDAO} to provide a more usable API.
68
 */
69
public class SecretDAO {
70
  private final DSLContext dslContext;
71
  private final SecretContentDAOFactory secretContentDAOFactory;
72
  private final SecretSeriesDAOFactory secretSeriesDAOFactory;
73
  private final GroupDAO.GroupDAOFactory groupDAOFactory;
74
  private final ContentCryptographer cryptographer;
75
  private final PermissionCheck permissionCheck;
76

77
  // this is the maximum length of a secret name, so that it will still fit in the 255 char limit
78
  // of the database field if it is deleted and auto-renamed
79
  private static final int SECRET_NAME_MAX_LENGTH = 195;
80

81
  // this is the maximum number of rows that will be deleted per-transaction by the endpoint
82
  // to permanently remove secrets
83
  private static final int MAX_ROWS_REMOVED_PER_TRANSACTION = 1000;
84

85
  public SecretDAO(
86
      DSLContext dslContext,
87
      SecretContentDAOFactory secretContentDAOFactory,
88
      SecretSeriesDAOFactory secretSeriesDAOFactory,
89
      GroupDAO.GroupDAOFactory groupDAOFactory,
90
      ContentCryptographer cryptographer,
91
      PermissionCheck permissionCheck) {
1✔
92
    this.dslContext = dslContext;
1✔
93
    this.secretContentDAOFactory = secretContentDAOFactory;
1✔
94
    this.secretSeriesDAOFactory = secretSeriesDAOFactory;
1✔
95
    this.groupDAOFactory = groupDAOFactory;
1✔
96
    this.cryptographer = cryptographer;
1✔
97
    this.permissionCheck = permissionCheck;
1✔
98
  }
1✔
99

100
  @VisibleForTesting
101
  public long createSecret(
102
      String name,
103
      String ownerName,
104
      String encryptedSecret,
105
      String hmac,
106
      String creator,
107
      Map<String, String> metadata,
108
      long expiry,
109
      String description,
110
      @Nullable String type,
111
      @Nullable Map<String, String> generationOptions) {
112

113
    return dslContext.transactionResult(configuration -> {
1✔
114
      // disallow use of a leading period in secret names
115
      // check is here because this is where all APIs converge on secret creation
116
      if (name.startsWith(".")) {
1✔
117
        throw new BadRequestException(format("secret cannot be created with name `%s` - secret "
1✔
118
            + "names cannot begin with a period", name));
119
      }
120

121
      // enforce a shorter max length than the db to ensure secrets renamed on deletion still fit
122
      if (name.length() > SECRET_NAME_MAX_LENGTH) {
1✔
123
        throw new BadRequestException(format("secret cannot be created with name `%s` - secret "
1✔
124
            + "names must be %d characters or less", name, SECRET_NAME_MAX_LENGTH));
1✔
125
      }
126

127
      long now = OffsetDateTime.now().toEpochSecond();
1✔
128

129
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
130
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
131

132
      Long ownerId = getOwnerId(configuration, ownerName);
1✔
133

134
      Optional<SecretSeries> secretSeries = secretSeriesDAO.getSecretSeriesByName(name);
1✔
135
      long secretId;
136
      if (secretSeries.isPresent()) {
1✔
137
        SecretSeries secretSeries1 = secretSeries.get();
1✔
138
        if (secretSeries1.currentVersion().isPresent()) {
1✔
139
          throw new DataAccessException(format("secret already present: %s", name));
1✔
140
        } else {
141
          // Unreachable unless the implementation of getSecretSeriesByName is changed
142
          throw new IllegalStateException(
×
143
              format("secret %s retrieved without current version set", name));
×
144
        }
145
      } else {
146
        secretId = secretSeriesDAO.createSecretSeries(
1✔
147
            name,
148
            ownerId,
149
            creator,
150
            description,
151
            type,
152
            generationOptions,
153
            now);
154
      }
155

156
      long secretContentId = secretContentDAO.createSecretContent(secretId, encryptedSecret, hmac,
1✔
157
          creator, metadata, expiry, now);
158
      secretSeriesDAO.setCurrentVersion(secretId, secretContentId, creator, now);
1✔
159

160
      return secretId;
1✔
161
    });
162
  }
163

164
  @VisibleForTesting
165
  public long createOrUpdateSecret(
166
      String name,
167
      String owner,
168
      String encryptedSecret,
169
      String hmac,
170
      String creator,
171
      Map<String, String> metadata,
172
      long expiry,
173
      String description,
174
      @Nullable String type,
175
      @Nullable Map<String, String> generationOptions) {
176
    return createOrUpdateSecret(
1✔
177
        dslContext,
178
        name,
179
        owner,
180
        encryptedSecret,
181
        hmac,
182
        creator,
183
        metadata,
184
        expiry,
185
        description,
186
        type,
187
        generationOptions
188
    );
189
  }
190

191
  @VisibleForTesting
192
  public long createOrUpdateSecret(
193
      DSLContext dslContext,
194
      String name,
195
      String owner,
196
      String encryptedSecret,
197
      String hmac,
198
      String creator,
199
      Map<String, String> metadata,
200
      long expiry,
201
      String description,
202
      @Nullable String type,
203
      @Nullable Map<String, String> generationOptions) {
204
    // SecretController should have already checked that the contents are not empty
205
    return dslContext.transactionResult(configuration -> {
1✔
206
      long now = OffsetDateTime.now().toEpochSecond();
1✔
207

208
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
209
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
210

211
      Long ownerId = getOwnerId(configuration, owner);
1✔
212

213
      Optional<SecretSeries> secretSeries = secretSeriesDAO.getSecretSeriesByName(name);
1✔
214
      long secretId;
215
      if (secretSeries.isPresent()) {
1✔
216
        SecretSeries secretSeries1 = secretSeries.get();
1✔
217
        secretId = secretSeries1.id();
1✔
218

219
        Long effectiveOwnerId = ownerId != null
1✔
220
            ? ownerId
1✔
221
            : getOwnerId(configuration, secretSeries1.owner());
1✔
222

223
        secretSeriesDAO.updateSecretSeries(
1✔
224
            secretId,
225
            name,
226
            effectiveOwnerId,
227
            creator,
228
            description,
229
            type,
230
            generationOptions,
231
            now);
232
      } else {
1✔
233
        secretId = secretSeriesDAO.createSecretSeries(
1✔
234
            name,
235
            ownerId,
236
            creator,
237
            description,
238
            type,
239
            generationOptions,
240
            now);
241
      }
242

243
      long secretContentId = secretContentDAO.createSecretContent(secretId, encryptedSecret, hmac,
1✔
244
          creator, metadata, expiry, now);
245
      secretSeriesDAO.setCurrentVersion(secretId, secretContentId, creator, now);
1✔
246

247
      return secretId;
1✔
248
    });
249
  }
250

251
  @VisibleForTesting
252
  public long partialUpdateSecret(String name, String creator,
253
      PartialUpdateSecretRequestV2 request) {
254
    return dslContext.transactionResult(configuration -> {
1✔
255
      long now = OffsetDateTime.now().toEpochSecond();
1✔
256

257
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
258
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
259

260
      // Get the current version of the secret, throwing exceptions if it is not found
261
      SecretSeries secretSeries = secretSeriesDAO.getSecretSeriesByName(name).orElseThrow(
1✔
262
          NotFoundException::new);
263
      Long currentVersion = secretSeries.currentVersion().orElseThrow(NotFoundException::new);
1✔
264
      SecretContent secretContent =
1✔
265
          secretContentDAO.getSecretContentById(currentVersion).orElseThrow(NotFoundException::new);
1✔
266

267
      long secretId = secretSeries.id();
1✔
268

269
      // Set the fields to the original series and current version's values or the request values if provided
270
      String description = request.descriptionPresent()
1✔
271
          ? request.description()
1✔
272
          : secretSeries.description();
1✔
273
      String type = request.typePresent()
1✔
274
          ? request.type()
1✔
275
          : secretSeries.type().orElse("");
1✔
276
      ImmutableMap<String, String> metadata = request.metadataPresent()
1✔
277
          ? request.metadata()
1✔
278
          : secretContent.metadata();
1✔
279
      Long expiry = request.expiryPresent()
1✔
280
          ? request.expiry()
1✔
281
          : secretContent.expiry();
1✔
282

283
      String owner = request.ownerPresent()
1✔
284
          ? request.owner()
1✔
285
          : secretSeries.owner();
1✔
286
      Long ownerId = getOwnerId(configuration, owner);
1✔
287

288
      String encryptedContent = secretContent.encryptedContent();
1✔
289
      String hmac = secretContent.hmac();
1✔
290
      // Mirrors hmac-creation in SecretController
291
      if (request.contentPresent()) {
1✔
292
        checkArgument(!request.content().isEmpty());
1✔
293

294
        hmac = cryptographer.computeHmac(
1✔
295
            request.content().getBytes(UTF_8), "hmackey"); // Compute HMAC on base64 encoded data
1✔
296
        if (hmac == null) {
1✔
297
          throw new ContentEncodingException("Error encoding content for SecretBuilder!");
×
298
        }
299
        encryptedContent = cryptographer.encryptionKeyDerivedFrom(name).encrypt(request.content());
1✔
300
      }
301

302
      secretSeriesDAO.updateSecretSeries(
1✔
303
          secretId,
304
          name,
305
          ownerId,
306
          creator,
307
          description,
308
          type,
309
          secretSeries.generationOptions(),
1✔
310
          now);
311

312
      long secretContentId = secretContentDAO.createSecretContent(secretId, encryptedContent, hmac,
1✔
313
          creator, metadata, expiry, now);
1✔
314
      secretSeriesDAO.setCurrentVersion(secretId, secretContentId, creator, now);
1✔
315

316
      return secretId;
1✔
317
    });
318
  }
319

320
  public boolean setExpiration(String name, Instant expiration) {
321
    return dslContext.transactionResult(configuration -> {
1✔
322
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
323

324
      Optional<SecretSeries> secretSeries = secretSeriesDAO.getSecretSeriesByName(name);
1✔
325
      if (secretSeries.isPresent()) {
1✔
326
        Optional<Long> currentVersion = secretSeries.get().currentVersion();
1✔
327
        if (currentVersion.isPresent()) {
1✔
328
          return secretSeriesDAO.setExpiration(currentVersion.get(), expiration) > 0;
1✔
329
        }
330
      }
331
      return false;
×
332
    });
333
  }
334

335
  /**
336
   * @param secretId external secret series id to look up secrets by.
337
   * @return Secret matching input parameters or Optional.absent().
338
   */
339
  public Optional<SecretSeriesAndContent> getSecretById(long secretId) {
340
    return dslContext.<Optional<SecretSeriesAndContent>>transactionResult(configuration -> {
1✔
341
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
342
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
343

344
      Optional<SecretSeries> series = secretSeriesDAO.getSecretSeriesById(secretId);
1✔
345
      if (series.isPresent() && series.get().currentVersion().isPresent()) {
1✔
346
        long secretContentId = series.get().currentVersion().get();
1✔
347
        Optional<SecretContent> contents = secretContentDAO.getSecretContentById(secretContentId);
1✔
348
        if (!contents.isPresent()) {
1✔
349
          throw new IllegalStateException(
1✔
350
              format("failed to fetch secret %d, content %d not found.", secretId,
1✔
351
                  secretContentId));
1✔
352
        }
353
        return Optional.of(SecretSeriesAndContent.of(series.get(), contents.get()));
1✔
354
      }
355
      return Optional.empty();
1✔
356
    });
357
  }
358

359
  public Optional<SecretSeriesAndContent> getSecretByName(String name) {
360
    return getSecretByName(dslContext, name);
1✔
361
  }
362

363
  /**
364
   * @param name of secret series to look up secrets by.
365
   * @return Secret matching input parameters or Optional.absent().
366
   */
367
  public Optional<SecretSeriesAndContent> getSecretByName(DSLContext dslContext, String name) {
368
    checkArgument(!name.isEmpty());
1✔
369

370
    // In the past, the two data fetches below were wrapped in a transaction. The transaction was
371
    // removed because jOOQ transactions doesn't play well with MySQL readonly connections
372
    // (see https://github.com/jOOQ/jOOQ/issues/3955).
373
    //
374
    // A possible work around is to write a transaction manager (see http://git.io/vkuFM)
375
    //
376
    // Removing the transaction however seems to be simpler and safe. The first data fetch's
377
    // secret.id is used for the second data fetch.
378
    //
379
    // A third way to work around this issue is to write a SQL join. Jooq makes it relatively easy,
380
    // but such joins hurt code re-use.
381
    SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
1✔
382
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
383

384
    Optional<SecretSeries> series = secretSeriesDAO.getSecretSeriesByName(name);
1✔
385
    if (series.isPresent() && series.get().currentVersion().isPresent()) {
1✔
386
      long secretContentId = series.get().currentVersion().get();
1✔
387
      Optional<SecretContent> secretContent =
1✔
388
          secretContentDAO.getSecretContentById(secretContentId);
1✔
389
      if (!secretContent.isPresent()) {
1✔
390
        return Optional.empty();
1✔
391
      }
392

393
      return Optional.of(SecretSeriesAndContent.of(series.get(), secretContent.get()));
1✔
394
    }
395
    return Optional.empty();
1✔
396
  }
397

398
  /**
399
   * @param names of secrets series to look up secrets by.
400
   * @return Secrets matching input parameters.
401
   */
402
  public List<SecretSeriesAndContent> getSecretsByName(List<String> names) {
403
    checkArgument(!names.isEmpty());
1✔
404

405
    SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
1✔
406
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
407

408
    List<SecretSeries> multipleSeries = secretSeriesDAO.getMultipleSecretSeriesByName(names);
1✔
409

410
    List<SecretSeriesAndContent> ret = new ArrayList<SecretSeriesAndContent>();
1✔
411

412
    for (SecretSeries series : multipleSeries) {
1✔
413
      if (series.currentVersion().isPresent()) {
1✔
414
        long secretContentId = series.currentVersion().get();
1✔
415
        Optional<SecretContent> secretContent =
1✔
416
                secretContentDAO.getSecretContentById(secretContentId);
1✔
417
        if (secretContent.isPresent()) {
1✔
418
          ret.add(SecretSeriesAndContent.of(series, secretContent.get()));
1✔
419
        } else {
420
          throw new NotFoundException("Secret not found.");
×
421
        }
422
      }
423
    }
1✔
424
    return ret;
1✔
425
  }
426

427
  /**
428
   * @param expireMaxTime the maximum expiration date for secrets to return (exclusive)
429
   * @param group the group secrets returned must be assigned to
430
   * @param expireMinTime the minimum expiration date for secrets to return (inclusive)
431
   * @param minName the minimum name (alphabetically) that will be returned for secrets
432
   *                expiring on expireMinTime (inclusive)
433
   * @param limit the maximum number of secrets to return
434
   *               which to start the list of returned secrets
435
   * @return list of secrets. can limit/sort by expiry, and for group if given
436
   */
437
  public ImmutableList<SecretSeriesAndContent> getSecrets(@Nullable Long expireMaxTime,
438
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
439
      @Nullable Integer limit) {
440
    return dslContext.transactionResult(configuration -> {
1✔
441
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
442
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
443

444
      ImmutableList.Builder<SecretSeriesAndContent> secretsBuilder = ImmutableList.builder();
1✔
445

446
      for (SecretSeries series : secretSeriesDAO.getSecretSeries(expireMaxTime, group,
1✔
447
          expireMinTime, minName, limit)) {
448
        SecretContent content =
1✔
449
            secretContentDAO.getSecretContentById(series.currentVersion().get()).get();
1✔
450
        SecretSeriesAndContent seriesAndContent = SecretSeriesAndContent.of(series, content);
1✔
451
        secretsBuilder.add(seriesAndContent);
1✔
452
      }
1✔
453

454
      return secretsBuilder.build();
1✔
455
    });
456
  }
457

458
  /**
459
   * @return A list of id, name
460
   */
461
  public ImmutableList<SimpleEntry<Long, String>> getSecretsNameOnly() {
462
    List<SimpleEntry<Long, String>> results = dslContext.select(SECRETS.ID, SECRETS.NAME)
1✔
463
        .from(SECRETS)
1✔
464
        .where(SECRETS.CURRENT.isNotNull())
1✔
465
        .fetchInto(Secrets.SECRETS)
1✔
466
        .map(r -> new SimpleEntry<>(r.getId(), r.getName()));
1✔
467
    return ImmutableList.copyOf(results);
1✔
468
  }
469

470
  /**
471
   * @param idx the first index to select in a list of secrets sorted by creation time
472
   * @param num the number of secrets after idx to select in the list of secrets
473
   * @param newestFirst if true, order the secrets from newest creation time to oldest
474
   * @return A list of secrets
475
   */
476
  public ImmutableList<SecretSeriesAndContent> getSecretsBatched(int idx, int num,
477
      boolean newestFirst) {
478
    return dslContext.transactionResult(configuration -> {
1✔
479
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
480
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
481

482
      ImmutableList.Builder<SecretSeriesAndContent> secretsBuilder = ImmutableList.builder();
1✔
483

484
      for (SecretSeries series : secretSeriesDAO.getSecretSeriesBatched(idx, num, newestFirst)) {
1✔
485
        SecretContent content =
1✔
486
            secretContentDAO.getSecretContentById(series.currentVersion().get()).get();
1✔
487
        SecretSeriesAndContent seriesAndContent = SecretSeriesAndContent.of(series, content);
1✔
488
        secretsBuilder.add(seriesAndContent);
1✔
489
      }
1✔
490

491
      return secretsBuilder.build();
1✔
492
    });
493
  }
494

495
  /**
496
   * @param name of secret series to look up secrets by.
497
   * @param versionIdx the first index to select in a list of versions sorted by creation time
498
   * @param numVersions the number of versions after versionIdx to select in the list of versions
499
   * @return Versions of a secret matching input parameters or Optional.absent().
500
   */
501
  public Optional<ImmutableList<SanitizedSecret>> getSecretVersionsByName(String name,
502
      int versionIdx, int numVersions) {
503
    checkArgument(!name.isEmpty());
1✔
504
    checkArgument(versionIdx >= 0);
1✔
505
    checkArgument(numVersions >= 0);
1✔
506

507
    SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
1✔
508
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
509

510
    Optional<SecretSeries> series = secretSeriesDAO.getSecretSeriesByName(name);
1✔
511
    if (series.isPresent()) {
1✔
512
      SecretSeries s = series.get();
1✔
513
      long secretId = s.id();
1✔
514
      Optional<ImmutableList<SecretContent>> contents =
1✔
515
          secretContentDAO.getSecretVersionsBySecretId(secretId, versionIdx, numVersions);
1✔
516
      if (contents.isPresent()) {
1✔
517
        ImmutableList.Builder<SanitizedSecret> b = new ImmutableList.Builder<>();
1✔
518
        b.addAll(contents.get()
1✔
519
            .stream()
1✔
520
            .map(c -> SanitizedSecret.fromSecretSeriesAndContent(SecretSeriesAndContent.of(s, c)))
1✔
521
            .collect(toList()));
1✔
522

523
        return Optional.of(b.build());
1✔
524
      }
525
    }
526

527
    return Optional.empty();
×
528
  }
529

530
  /**
531
   *
532
   * @param secretId, the secret's id
533
   * @param versionIdx the first index to select in a list of versions sorted by creation time
534
   * @param numVersions the number of versions after versionIdx to select in the list of versions
535
   * @return all versions of a deleted secret, including the secret's content for each version,
536
   * matching input parameters or Optional.absent().
537
   */
538
  public Optional<ImmutableList<SecretSeriesAndContent>> getDeletedSecretVersionsBySecretId(long secretId,
539
      int versionIdx, int numVersions) {
540
    checkArgument(versionIdx >= 0);
1✔
541
    checkArgument(numVersions >= 0);
1✔
542

543
    SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
1✔
544
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
545

546
    Optional<SecretSeries> series = secretSeriesDAO.getDeletedSecretSeriesById(secretId);
1✔
547
    if (series.isPresent()) {
1✔
548
      SecretSeries s = series.get();
1✔
549
      Optional<ImmutableList<SecretContent>> contents =
1✔
550
          secretContentDAO.getSecretVersionsBySecretId(secretId, versionIdx, numVersions);
1✔
551
      if (contents.isPresent()) {
1✔
552
        ImmutableList.Builder<SecretSeriesAndContent> b = new ImmutableList.Builder<>();
1✔
553
        b.addAll(contents.get()
1✔
554
            .stream()
1✔
555
            .map(c ->SecretSeriesAndContent.of(s, c))
1✔
556
            .collect(toList()));
1✔
557

558
        return Optional.of(b.build());
1✔
559
      }
560
    }
561

562
    return Optional.empty();
×
563
  }
564

565
  public List<SecretSeries> getSecretsWithDeletedName(String name) {
566
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
567

568
    return secretSeriesDAO.getSecretSeriesByDeletedName(name);
1✔
569
  }
570

571
  public Optional<SecretSeries> getDeletedSecretsWithID(long id) {
572
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
×
573
    return secretSeriesDAO.getDeletedSecretSeriesById(id);
×
574
  }
575

576
  /**
577
   * @param name of secret series for which to reset secret version
578
   * @param versionId The identifier for the desired current version
579
   * @param updater the user to be linked to this update
580
   * @throws NotFoundException if secret not found
581
   */
582
  public void setCurrentSecretVersionByName(String name, long versionId, String updater) {
583
    checkArgument(!name.isEmpty());
1✔
584

585
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
586
    SecretSeries series = secretSeriesDAO.getSecretSeriesByName(name).orElseThrow(
1✔
587
        NotFoundException::new);
588
    secretSeriesDAO.setCurrentVersion(series.id(), versionId, updater,
1✔
589
        OffsetDateTime.now().toEpochSecond());
1✔
590
  }
1✔
591

592
  /**
593
   * Deletes the series and all associated version of the given secret series name.
594
   *
595
   * @param name of secret series to delete.
596
   */
597
  public void deleteSecretsByName(String name) {
598
    deleteSecretsByName(name, SecretDeletionMode.SOFT);
1✔
599
  }
1✔
600

601
  public void deleteSecretsByName(String name, SecretDeletionMode mode) {
602
    checkArgument(!name.isEmpty());
1✔
603

604
    switch(mode) {
1✔
605
      case HARD:
606
        secretSeriesDAOFactory.using(dslContext.configuration())
1✔
607
            .hardDeleteSecretSeriesByName(name);
1✔
608
        break;
1✔
609
      case SOFT:
610
        secretSeriesDAOFactory.using(dslContext.configuration())
1✔
611
            .softDeleteSecretSeriesByName(name);
1✔
612
        break;
1✔
613
      default:
614
        throw new IllegalArgumentException(String.format("Unknown secret deletion mode: %s", mode));
×
615
    }
616
  }
1✔
617

618
  public void undeleteSecret(long secretId) {
619
    secretSeriesDAOFactory.using(dslContext.configuration())
×
620
        .undeleteSoftDeletedSecretSeriesByID(secretId);
×
621
  }
×
622

623
  /**
624
   * Renames the secret, specified by the secret id, to the name provided
625
   * We check to make sure there are no other secrets that have the same name - if so,
626
   * we throw an exception to prevent multiple secrets from having the same name
627
   * @param secretId
628
   * @param name
629
   */
630
  public void renameSecretById(long secretId, String name, String creator) {
631
    checkArgument(!name.isEmpty());
1✔
632
    Optional<SecretSeries> secretSeriesWithName =
1✔
633
        secretSeriesDAOFactory.using(dslContext.configuration()).getSecretSeriesByName(name);
1✔
634
    if(secretSeriesWithName.isPresent()) {
1✔
635
      throw new ConflictException(
1✔
636
          String.format("name %s already used by an existing secret in keywhiz", name));
1✔
637
    }
638

639
    secretSeriesDAOFactory.using(dslContext.configuration())
1✔
640
        .renameSecretSeriesById(secretId, name, creator, OffsetDateTime.now().toEpochSecond());
1✔
641
  }
1✔
642

643
  /**
644
   * Updates the Secret Content ID for the given Secret
645
   *
646
   */
647
  public void setCurrentSecretVersionBySecretId(long secretId, long secretContentId, String updater) {
648
    secretSeriesDAOFactory.using(dslContext.configuration())
1✔
649
        .setCurrentVersion(secretId, secretContentId, updater,
1✔
650
        OffsetDateTime.now().toEpochSecond());
1✔
651
  }
1✔
652

653
  /**
654
   * @return the total number of deleted secrets.
655
   */
656
  public int countDeletedSecrets() {
657
    return secretSeriesDAOFactory.using(dslContext.configuration())
1✔
658
        .countDeletedSecretSeries();
1✔
659
  }
660

661
  /**
662
   * @param deleteBefore the cutoff date; secrets deleted before this date will be counted
663
   * @return the number of secrets deleted before the specified cutoff.
664
   */
665
  public int countSecretsDeletedBeforeDate(DateTime deleteBefore) {
666
    checkArgument(deleteBefore != null);
1✔
667

668
    // identify the secrets deleted before this date
669
    return secretSeriesDAOFactory.using(dslContext.configuration())
1✔
670
        .getIdsForSecretSeriesDeletedBeforeDate(deleteBefore)
1✔
671
        .size();
1✔
672
  }
673

674
  /**
675
   * Permanently removes the series and all secrets-contents records ("versions") which were deleted
676
   * before the given date.
677
   *
678
   * Unlike the "delete" endpoints above, THIS REMOVAL IS PERMANENT and cannot be undone by editing
679
   * the database to restore the "current" entries.
680
   *
681
   * @param deletedBefore the cutoff date; secrets deleted before this date will be removed from the
682
   * database
683
   * @param sleepMillis how many milliseconds to sleep between each batch of removals
684
   * @throws InterruptedException if interrupted while sleeping between batches
685
   */
686
  public void dangerPermanentlyRemoveSecretsDeletedBeforeDate(DateTime deletedBefore,
687
      int sleepMillis) throws InterruptedException {
688
    checkArgument(deletedBefore != null);
1✔
689
    SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());
1✔
690
    SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
1✔
691

692
    // identify the secrets deleted before this date
693
    List<Long> ids = secretSeriesDAO.getIdsForSecretSeriesDeletedBeforeDate(deletedBefore);
1✔
694

695
    // batch the list of secrets to be removed, to reduce load on the database
696
    List<List<Long>> partitionedIds = Lists.partition(ids, MAX_ROWS_REMOVED_PER_TRANSACTION);
1✔
697
    for (List<Long> idBatch : partitionedIds) {
1✔
698
      // permanently remove the `secrets_contents` entries originally associated with these secrets
699
      secretContentDAO.dangerPermanentlyRemoveRecordsForGivenSecretsIDs(idBatch);
1✔
700

701
      // permanently remove the `secrets` entries for these secrets
702
      secretSeriesDAO.dangerPermanentlyRemoveRecordsForGivenIDs(idBatch);
1✔
703

704
      // sleep
705
      Thread.sleep(sleepMillis);
1✔
706
    }
1✔
707
  }
1✔
708

709
  private Long getOwnerId(Configuration configuration, String ownerName) {
710
    if (ownerName == null || ownerName.length() == 0) {
1✔
711
      return null;
1✔
712
    }
713

714
    GroupDAO groupDAO = groupDAOFactory.using(configuration);
1✔
715
    Optional<Group> maybeGroup = groupDAO.getGroup(ownerName);
1✔
716

717
    if (maybeGroup.isEmpty()) {
1✔
718
      throw new IllegalArgumentException(String.format("Unknown owner %s", ownerName));
1✔
719
    }
720

721
    return maybeGroup.get().getId();
1✔
722
  }
723

724
  public static class SecretDAOFactory implements DAOFactory<SecretDAO> {
725
    private final DSLContext jooq;
726
    private final DSLContext readonlyJooq;
727
    private final SecretContentDAOFactory secretContentDAOFactory;
728
    private final SecretSeriesDAOFactory secretSeriesDAOFactory;
729
    private final GroupDAO.GroupDAOFactory groupDAOFactory;
730
    private final ContentCryptographer cryptographer;
731
    private final PermissionCheck permissionCheck;
732

733
    @Inject public SecretDAOFactory(
734
        DSLContext jooq,
735
        @Readonly DSLContext readonlyJooq,
736
        SecretContentDAOFactory secretContentDAOFactory,
737
        SecretSeriesDAOFactory secretSeriesDAOFactory,
738
        GroupDAO.GroupDAOFactory groupDAOFactory,
739
        ContentCryptographer cryptographer,
740
        PermissionCheck permissionCheck) {
1✔
741
      this.jooq = jooq;
1✔
742
      this.readonlyJooq = readonlyJooq;
1✔
743
      this.secretContentDAOFactory = secretContentDAOFactory;
1✔
744
      this.secretSeriesDAOFactory = secretSeriesDAOFactory;
1✔
745
      this.groupDAOFactory = groupDAOFactory;
1✔
746
      this.cryptographer = cryptographer;
1✔
747
      this.permissionCheck = permissionCheck;
1✔
748
    }
1✔
749

750
    @Override public SecretDAO readwrite() {
751
      return new SecretDAO(
1✔
752
          jooq,
753
          secretContentDAOFactory,
754
          secretSeriesDAOFactory,
755
          groupDAOFactory,
756
          cryptographer,
757
          permissionCheck);
758
    }
759

760
    @Override public SecretDAO readonly() {
761
      return new SecretDAO(
1✔
762
          readonlyJooq,
763
          secretContentDAOFactory,
764
          secretSeriesDAOFactory,
765
          groupDAOFactory,
766
          cryptographer,
767
          permissionCheck);
768
    }
769

770
    @Override public SecretDAO using(Configuration configuration) {
771
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
×
772
      return new SecretDAO(
×
773
          dslContext,
774
          secretContentDAOFactory,
775
          secretSeriesDAOFactory,
776
          groupDAOFactory,
777
          cryptographer,
778
          permissionCheck);
779
    }
780
  }
781
}
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