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

square / keywhiz / 4245513118

pending completion
4245513118

Pull #1197

github

GitHub
Merge a271a68b8 into 6f7f80e31
Pull Request #1197: Deleted Secrets - Read from new tables

61 of 61 new or added lines in 2 files covered. (100.0%)

5298 of 7054 relevant lines covered (75.11%)

0.75 hits per line

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

92.58
/server/src/main/java/keywhiz/service/daos/SecretSeriesDAO.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.fasterxml.jackson.core.JsonProcessingException;
20
import com.fasterxml.jackson.databind.ObjectMapper;
21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.base.Throwables;
23
import com.google.common.collect.ImmutableList;
24
import com.google.common.collect.ImmutableMap;
25
import java.util.Set;
26
import java.util.stream.Collectors;
27
import java.util.stream.Stream;
28
import keywhiz.api.model.Group;
29
import keywhiz.api.model.SecretSeries;
30
import keywhiz.jooq.tables.records.DeletedSecretsRecord;
31
import keywhiz.jooq.tables.records.SecretsContentRecord;
32
import keywhiz.jooq.tables.records.SecretsRecord;
33
import keywhiz.service.config.Readonly;
34
import keywhiz.service.crypto.RowHmacGenerator;
35
import org.joda.time.DateTime;
36
import org.jooq.Configuration;
37
import org.jooq.DSLContext;
38
import org.jooq.Record;
39
import org.jooq.SelectQuery;
40
import org.jooq.impl.DSL;
41

42
import javax.annotation.Nullable;
43
import javax.inject.Inject;
44
import javax.ws.rs.BadRequestException;
45
import java.time.Instant;
46
import java.time.OffsetDateTime;
47
import java.util.List;
48
import java.util.Map;
49
import java.util.Optional;
50
import java.util.UUID;
51

52
import static com.google.common.base.Preconditions.checkNotNull;
53
import static keywhiz.jooq.tables.Accessgrants.ACCESSGRANTS;
54
import static keywhiz.jooq.tables.Groups.GROUPS;
55
import static keywhiz.jooq.tables.Secrets.SECRETS;
56
import static keywhiz.jooq.tables.DeletedSecrets.DELETED_SECRETS;
57
import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT;
58

59
/**
60
 * Interacts with 'secrets' table and actions on {@link SecretSeries} entities.
61
 */
62
public class SecretSeriesDAO {
63
  private final DSLContext dslContext;
64
  private final ObjectMapper mapper;
65
  private final SecretSeriesMapper secretSeriesMapper;
66
  private final RowHmacGenerator rowHmacGenerator;
67

68
  private SecretSeriesDAO(
69
      DSLContext dslContext,
70
      ObjectMapper mapper,
71
      SecretSeriesMapper secretSeriesMapper,
72
      RowHmacGenerator rowHmacGenerator) {
1✔
73
    this.dslContext = dslContext;
1✔
74
    this.mapper = mapper;
1✔
75
    this.secretSeriesMapper = secretSeriesMapper;
1✔
76
    this.rowHmacGenerator = rowHmacGenerator;
1✔
77
  }
1✔
78

79
  public boolean secretSeriesExists(String name) {
80
    return dslContext.fetchExists(SECRETS, SECRETS.NAME.eq(name));
1✔
81
  }
82

83
  long createSecretSeries(
84
      String name,
85
      Long ownerId,
86
      String creator,
87
      String description,
88
      @Nullable String type,
89
      @Nullable Map<String, String> generationOptions,
90
      long now) {
91
    long generatedId = rowHmacGenerator.getNextLongSecure();
1✔
92
    return createSecretSeries(
1✔
93
        generatedId,
94
        name,
95
        ownerId,
96
        creator,
97
        description,
98
        type,
99
        generationOptions,
100
        now);
101
  }
102

103
  @VisibleForTesting
104
  long createSecretSeries(
105
      long id,
106
      String name,
107
      Long ownerId,
108
      String creator,
109
      String description,
110
      @Nullable String type,
111
      @Nullable Map<String, String> generationOptions,
112
      long now) {
113
    SecretsRecord r = dslContext.newRecord(SECRETS);
1✔
114

115
    String rowHmac = computeRowHmac(id, name);
1✔
116

117
    r.setId(id);
1✔
118
    r.setName(name);
1✔
119
    r.setOwner(ownerId);
1✔
120
    r.setDescription(description);
1✔
121
    r.setCreatedby(creator);
1✔
122
    r.setCreatedat(now);
1✔
123
    r.setUpdatedby(creator);
1✔
124
    r.setUpdatedat(now);
1✔
125
    r.setType(type);
1✔
126
    r.setRowHmac(rowHmac);
1✔
127
    r.setOptions(getOptionsField(generationOptions));
1✔
128

129
    r.store();
1✔
130

131
    return r.getId();
1✔
132
  }
133

134
  @VisibleForTesting
135
  public long createDeletedSecretSeries(
136
      String name,
137
      Long ownerId,
138
      String creator,
139
      String description,
140
      @Nullable Long currentVersionID,
141
      @Nullable String type,
142
      @Nullable Map<String, String> generationOptions,
143
      long now) {
144
    long generatedId = rowHmacGenerator.getNextLongSecure();
1✔
145
    return createDeletedSecretSeries(
1✔
146
        generatedId,
147
        name,
148
        ownerId,
149
        creator,
150
        description,
151
        currentVersionID,
152
        type,
153
        generationOptions,
154
        now);
155
  }
156

157
  @VisibleForTesting
158
  public long createDeletedSecretSeries(
159
      long id,
160
      String name,
161
      Long ownerId,
162
      String creator,
163
      String description,
164
      @Nullable Long currentVersionID,
165
      @Nullable String type,
166
      @Nullable Map<String, String> generationOptions,
167
      long now
168
  ) {
169
    DeletedSecretsRecord record = dslContext.newRecord(DELETED_SECRETS);
1✔
170

171
    String rowHmac = computeRowHmacForDeletedSecret(id, name);
1✔
172

173
    record.setId(id);
1✔
174
    record.setName(name);
1✔
175
    record.setOwner(ownerId);
1✔
176
    record.setDescription(description);
1✔
177
    record.setCurrent(currentVersionID);
1✔
178
    record.setCreatedby(creator);
1✔
179
    record.setCreatedat(now);
1✔
180
    record.setUpdatedby(creator);
1✔
181
    record.setUpdatedat(now);
1✔
182
    record.setType(type);
1✔
183
    record.setRowHmac(rowHmac);
1✔
184
    record.setOptions(getOptionsField(generationOptions));
1✔
185

186
    record.store();
1✔
187

188
    return record.getId();
1✔
189
  }
190

191
  private String getOptionsField(
192
      @Nullable Map<String, String> generationOptions
193
  ) {
194
    if (generationOptions == null) {
1✔
195
      return "{}";
1✔
196
    }
197
    try {
198
      return mapper.writeValueAsString(generationOptions);
1✔
199
    } catch (JsonProcessingException e) {
×
200
      // Serialization of a Map<String, String> should never fail.
201
      throw Throwables.propagate(e);
×
202
    }
203
  }
204

205
  void updateSecretSeries(
206
      long secretId,
207
      String name,
208
      Long ownerId,
209
      String creator,
210
      String description,
211
      @Nullable String type,
212
      @Nullable Map<String, String> generationOptions,
213
      long now) {
214

215
    if (generationOptions == null) {
1✔
216
      generationOptions = ImmutableMap.of();
1✔
217
    }
218

219
    try {
220
      String rowHmac = computeRowHmac(secretId, name);
1✔
221

222
      dslContext.update(SECRETS)
1✔
223
          .set(SECRETS.NAME, name)
1✔
224
          .set(SECRETS.OWNER, ownerId)
1✔
225
          .set(SECRETS.DESCRIPTION, description)
1✔
226
          .set(SECRETS.UPDATEDBY, creator)
1✔
227
          .set(SECRETS.UPDATEDAT, now)
1✔
228
          .set(SECRETS.TYPE, type)
1✔
229
          .set(SECRETS.OPTIONS, mapper.writeValueAsString(generationOptions))
1✔
230
          .set(SECRETS.ROW_HMAC, rowHmac)
1✔
231
          .where(SECRETS.ID.eq(secretId))
1✔
232
          .execute();
1✔
233
    } catch (JsonProcessingException e) {
×
234
      // Serialization of a Map<String, String> can never fail.
235
      throw Throwables.propagate(e);
×
236
    }
1✔
237
  }
1✔
238

239
  public int setExpiration(long secretContentId, Instant expiration) {
240
    return dslContext.transactionResult(configuration -> {
1✔
241
      SecretsContentRecord content = dslContext.select(SECRETS_CONTENT.EXPIRY)
1✔
242
          .from(SECRETS_CONTENT)
1✔
243
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
244
          .forUpdate()
1✔
245
          .fetchOneInto(SecretsContentRecord.class);
1✔
246

247
      if (content == null) {
1✔
248
        return 0;
×
249
      }
250

251
      Long currentExpiry = content.getExpiry();
1✔
252
      long epochSeconds = expiration.getEpochSecond();
1✔
253

254
      Long updatedExpiry = (currentExpiry == null || currentExpiry == 0)
1✔
255
          ? epochSeconds
1✔
256
          : Math.min(currentExpiry, epochSeconds);
1✔
257

258
      int contentsUpdated = dslContext.update(SECRETS_CONTENT)
1✔
259
          .set(SECRETS_CONTENT.EXPIRY, updatedExpiry)
1✔
260
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
261
          .execute();
1✔
262

263
      int secretsUpdated = dslContext.update(SECRETS)
1✔
264
          .set(SECRETS.EXPIRY, updatedExpiry)
1✔
265
          .where(SECRETS.CURRENT.eq(secretContentId))
1✔
266
          .execute();
1✔
267

268
      return contentsUpdated + secretsUpdated;
1✔
269
    });
270
  }
271

272
  public int setRowHmacByName(String secretName, String hmac) {
273
    return dslContext.update(SECRETS)
1✔
274
        .set(SECRETS.ROW_HMAC, hmac)
1✔
275
        .where(SECRETS.NAME.eq(secretName))
1✔
276
        .execute();
1✔
277
  }
278

279
  public int setHmac(long secretContentId, String hmac) {
280
    return dslContext.update(SECRETS_CONTENT)
×
281
        .set(SECRETS_CONTENT.CONTENT_HMAC, hmac)
×
282
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
×
283
        .execute();
×
284
  }
285

286
  public int setCurrentVersion(long secretId, long secretContentId, String updater, long now) {
287
    SecretsContentRecord r = dslContext
1✔
288
        .select(
1✔
289
            SECRETS_CONTENT.SECRETID,
290
            SECRETS_CONTENT.EXPIRY)
291
        .from(SECRETS_CONTENT)
1✔
292
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
293
        .fetchOneInto(SECRETS_CONTENT);
1✔
294

295
    if (r == null) {
1✔
296
      throw new BadRequestException(
1✔
297
          String.format("The requested version %d is not a known version of this secret",
1✔
298
              secretContentId));
1✔
299
    }
300

301
    long checkId = r.getSecretid();
1✔
302
    if (checkId != secretId) {
1✔
303
      throw new IllegalStateException(String.format(
1✔
304
          "tried to reset secret with id %d to version %d, but this version is not associated with this secret",
305
          secretId, secretContentId));
1✔
306
    }
307

308
    return dslContext.update(SECRETS)
1✔
309
        .set(SECRETS.CURRENT, secretContentId)
1✔
310
        .set(SECRETS.EXPIRY, r.getExpiry())
1✔
311
        .set(SECRETS.UPDATEDBY, updater)
1✔
312
        .set(SECRETS.UPDATEDAT, now)
1✔
313
        .where(SECRETS.ID.eq(secretId))
1✔
314
        .execute();
1✔
315
  }
316

317
  public Optional<SecretSeries> getSecretSeriesById(long id) {
318
    SecretsRecord r = getSecretSeriesRecordById(id);
1✔
319
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
320
  }
321

322
  @VisibleForTesting
323
  SecretsRecord getSecretSeriesRecordById(long id) {
324
    return dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
325
  }
326

327
  public Optional<SecretSeries> getDeletedSecretSeriesById(long id) {
328
    Optional<SecretSeries> fromDeletedTable = getDeletedSecretSeriesFromDeletedSecretsTable(id);
1✔
329
    if (fromDeletedTable.isPresent()) {
1✔
330
      return fromDeletedTable;
1✔
331
    }
332
    return getDeletedSecretSeriesFromMainSecretsTable(id);
1✔
333
  }
334

335
  private Optional<SecretSeries> getDeletedSecretSeriesFromMainSecretsTable(long id) {
336
    SecretsRecord r =
1✔
337
        dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNull()));
1✔
338
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
339
  }
340

341
  private Optional<SecretSeries> getDeletedSecretSeriesFromDeletedSecretsTable(long id) {
342
    DeletedSecretsRecord r =
1✔
343
        dslContext.fetchOne(DELETED_SECRETS, DELETED_SECRETS.ID.eq(id));
1✔
344
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
345
  }
346

347
  public Optional<SecretSeries> getSecretSeriesByName(String name) {
348
    SecretsRecord r =
1✔
349
        dslContext.fetchOne(SECRETS, SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()));
1✔
350
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
351
  }
352

353
  public List<SecretSeries> getSecretSeriesByDeletedName(String name) {
354
    List<SecretSeries> fromDeletedSecretsTable =
1✔
355
        getDeletedSecretSeriesFromDeletedSecretsTable(name);
1✔
356
    Set<Long> idsFromDeletedSecretsTable =
1✔
357
        fromDeletedSecretsTable.stream().map(SecretSeries::id).collect(Collectors.toSet());
1✔
358
    return Stream.concat(fromDeletedSecretsTable.stream(),
1✔
359
            getDeletedSecretSeriesFromMainSecretsTable(name).stream()
1✔
360
                // If a secret series exists in both tables, only include the copy from `deleted_secrets`
361
                // rather than the copy from `secrets` since it contains more information.
362
                .filter(secretSeries -> !idsFromDeletedSecretsTable.contains(secretSeries.id())))
1✔
363
        .collect(Collectors.toList());
1✔
364
  }
365

366
  private List<SecretSeries> getDeletedSecretSeriesFromMainSecretsTable(String name) {
367
    String lookup = "." + name + ".%";
1✔
368
    return dslContext.fetch(SECRETS, SECRETS.NAME.like(lookup).and(SECRETS.CURRENT.isNull())).map(secretSeriesMapper::map);
1✔
369
  }
370

371
  private List<SecretSeries> getDeletedSecretSeriesFromDeletedSecretsTable(String name) {
372
    return dslContext.fetch(
1✔
373
        DELETED_SECRETS,
374
        DELETED_SECRETS.NAME.eq(name)
1✔
375
    ).map(secretSeriesMapper::map);
1✔
376
  }
377

378
  public List<SecretSeries> getMultipleSecretSeriesByName(List<String> names) {
379
    return dslContext.fetch(SECRETS, SECRETS.NAME.in(names).and(SECRETS.CURRENT.isNotNull())).map(secretSeriesMapper::map);
1✔
380
  }
381

382
  public List<String> listExpiringSecretNames(Instant notAfterInclusive) {
383
    List<String> expiringSecretNames = dslContext
1✔
384
        .select()
1✔
385
        .from(SECRETS)
1✔
386
        .where(SECRETS.CURRENT.isNotNull())
1✔
387
        .and(SECRETS.EXPIRY.greaterOrEqual(Instant.now().getEpochSecond()))
1✔
388
        .and(SECRETS.EXPIRY.lessThan(notAfterInclusive.getEpochSecond()))
1✔
389
        .fetch(SECRETS.NAME);
1✔
390
    return ImmutableList.copyOf(expiringSecretNames);
1✔
391
  }
392

393
  public ImmutableList<SecretSeries> getSecretSeries(@Nullable Long expireMaxTime,
394
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
395
      @Nullable Integer limit) {
396

397
    SelectQuery<Record> select = dslContext
1✔
398
          .select(SECRETS.fields())
1✔
399
          .from(SECRETS)
1✔
400
          .where(SECRETS.CURRENT.isNotNull())
1✔
401
          .getQuery();
1✔
402
    select.addOrderBy(SECRETS.EXPIRY.asc(), SECRETS.NAME.asc());
1✔
403

404
    // Set an upper bound on expiration dates
405
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
406
      // Set a lower bound of "now" on the expiration only if it isn't configured separately
407
      if (expireMinTime == null || expireMinTime == 0) {
1✔
408
        long now = System.currentTimeMillis() / 1000L;
1✔
409
        select.addConditions(SECRETS.EXPIRY.greaterOrEqual(now));
1✔
410
      }
411
      select.addConditions(SECRETS.EXPIRY.lessThan(expireMaxTime));
1✔
412
    }
413

414
    if (expireMinTime != null && expireMinTime > 0) {
1✔
415
      // set a lower bound on expiration dates, using the secret name as a tiebreaker
416
      select.addConditions(SECRETS.EXPIRY.greaterThan(expireMinTime)
1✔
417
          .or(SECRETS.EXPIRY.eq(expireMinTime)
1✔
418
              .and(SECRETS.NAME.greaterOrEqual(minName))));
1✔
419
    }
420

421
    if (group != null) {
1✔
422
      select.addJoin(ACCESSGRANTS, SECRETS.ID.eq(ACCESSGRANTS.SECRETID));
1✔
423
      select.addJoin(GROUPS, GROUPS.ID.eq(ACCESSGRANTS.GROUPID));
1✔
424
      select.addConditions(GROUPS.NAME.eq(group.getName()));
1✔
425
    }
426

427
    if (limit != null && limit >= 0) {
1✔
428
      select.addLimit(limit);
1✔
429
    }
430

431
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
432
    return ImmutableList.copyOf(r);
1✔
433
  }
434

435
  public ImmutableList<SecretSeries> getSecretSeriesBatched(int idx, int num, boolean newestFirst) {
436
    SelectQuery<Record> select = dslContext
1✔
437
        .select()
1✔
438
        .from(SECRETS)
1✔
439
        .join(SECRETS_CONTENT)
1✔
440
        .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
441
        .where(SECRETS.CURRENT.isNotNull())
1✔
442
        .getQuery();
1✔
443
    if (newestFirst) {
1✔
444
      select.addOrderBy(SECRETS.CREATEDAT.desc());
1✔
445
    } else {
446
      select.addOrderBy(SECRETS.CREATEDAT.asc());
1✔
447
    }
448
    select.addLimit(idx, num);
1✔
449

450
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
451
    return ImmutableList.copyOf(r);
1✔
452
  }
453

454
  public void hardDeleteSecretSeriesByName(String name) {
455
    dslContext.transaction(configuration -> {
1✔
456
      DSLContext dslContext = DSL.using(configuration);
1✔
457

458
      SecretsRecord record = dslContext.select()
1✔
459
          .from(SECRETS)
1✔
460
          .where(SECRETS.NAME.eq(name))
1✔
461
          .forUpdate()
1✔
462
          .fetchOneInto(SECRETS);
1✔
463

464
      hardDeleteSecretSeries(dslContext, record);
1✔
465
    });
1✔
466
  }
1✔
467

468
  public void hardDeleteSecretSeriesById(Long id) {
469
    dslContext.transaction(configuration -> {
×
470
      DSLContext dslContext = DSL.using(configuration);
×
471

472
      SecretsRecord record = dslContext.select()
×
473
          .from(SECRETS)
×
474
          .where(SECRETS.ID.eq(id))
×
475
          .forUpdate()
×
476
          .fetchOneInto(SECRETS);
×
477

478
      hardDeleteSecretSeries(dslContext, record);
×
479
    });
×
480
  }
×
481

482
  private static void hardDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
483
    if (record == null) {
1✔
484
      return;
×
485
    }
486

487
    dslContext.deleteFrom(SECRETS_CONTENT)
1✔
488
        .where(SECRETS_CONTENT.SECRETID.eq(record.getId()))
1✔
489
        .execute();
1✔
490
    dslContext.deleteFrom(SECRETS)
1✔
491
        .where(SECRETS.ID.eq(record.getId()))
1✔
492
        .execute();
1✔
493
    dslContext.deleteFrom(ACCESSGRANTS)
1✔
494
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
495
        .execute();
1✔
496
  }
1✔
497

498
  public void softDeleteSecretSeriesByName(String name) {
499
    dslContext.transaction(configuration -> {
1✔
500
      // find the record and lock it until this transaction is complete
501
      SecretsRecord record = DSL.using(configuration)
1✔
502
          .select()
1✔
503
          .from(SECRETS)
1✔
504
          .where(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()))
1✔
505
          .forUpdate()
1✔
506
          .fetchOneInto(SECRETS);
1✔
507

508
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
509
    });
1✔
510
  }
1✔
511

512
  public void softDeleteSecretSeriesById(long id) {
513
    dslContext.transaction(configuration -> {
1✔
514
      // find the record and lock it until this transaction is complete
515
      SecretsRecord record = DSL.using(configuration)
1✔
516
          .select()
1✔
517
          .from(SECRETS)
1✔
518
          .where(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()))
1✔
519
          .forUpdate()
1✔
520
          .fetchOneInto(SECRETS);
1✔
521

522
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
523
    });
1✔
524
  }
1✔
525

526
  private static void softDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
527
    if (record == null) {
1✔
528
      return;
×
529
    }
530

531
    long now = OffsetDateTime.now().toEpochSecond();
1✔
532

533
    dslContext
1✔
534
        .update(SECRETS)
1✔
535
        .set(SECRETS.NAME, transformNameForDeletion(record.getName()))
1✔
536
        .set(SECRETS.CURRENT, (Long) null)
1✔
537
        .set(SECRETS.UPDATEDAT, now)
1✔
538
        .where(SECRETS.ID.eq(record.getId()))
1✔
539
        .execute();
1✔
540

541
    dslContext
1✔
542
        .delete(ACCESSGRANTS)
1✔
543
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
544
        .execute();
1✔
545
  }
1✔
546

547
  public void renameSecretSeriesById(long secretId, String name, String creator, long now) {
548
    String rowHmac = computeRowHmac(secretId, name);
1✔
549
    dslContext.update(SECRETS)
1✔
550
        .set(SECRETS.NAME, name)
1✔
551
        .set(SECRETS.ROW_HMAC, rowHmac)
1✔
552
        .set(SECRETS.UPDATEDBY, creator)
1✔
553
        .set(SECRETS.UPDATEDAT, now)
1✔
554
        .where(SECRETS.ID.eq(secretId))
1✔
555
        .execute();
1✔
556
  }
1✔
557

558
  /**
559
   * @return the number of deleted secret series
560
   */
561
  public int countDeletedSecretSeries() {
562
    return dslContext.selectCount()
1✔
563
        .from(SECRETS)
1✔
564
        .where(SECRETS.CURRENT.isNull())
1✔
565
        .fetchOne()
1✔
566
        .value1();
1✔
567
  }
568

569
  /**
570
   * Identify all secret series which were deleted before the given date.
571
   *
572
   * @param deleteBefore the cutoff date; secrets deleted before this date will be returned
573
   * @return IDs for secret series deleted before this date
574
   */
575
  public List<Long> getIdsForSecretSeriesDeletedBeforeDate(DateTime deleteBefore) {
576
    long deleteBeforeSeconds = deleteBefore.getMillis() / 1000;
1✔
577
    return dslContext.select(SECRETS.ID)
1✔
578
        .from(SECRETS)
1✔
579
        .where(SECRETS.CURRENT.isNull())
1✔
580
        .and(SECRETS.UPDATEDAT.le(deleteBeforeSeconds))
1✔
581
        .fetch(SECRETS.ID);
1✔
582
  }
583

584
  /**
585
   * PERMANENTLY REMOVE database records from `secrets` which have the given list of IDs. Does not
586
   * affect the `secrets_content` table.
587
   *
588
   * @param ids the IDs in the `secrets` table to be PERMANENTLY REMOVED
589
   * @return the number of records which were removed
590
   */
591
  public long dangerPermanentlyRemoveRecordsForGivenIDs(List<Long> ids) {
592
    var deletedCount = new Object(){ long val = 0; };
1✔
593
    dslContext.transaction(configuration -> {
1✔
594
      dslContext.deleteFrom(DELETED_SECRETS)
1✔
595
          .where(DELETED_SECRETS.ID.in(ids))
1✔
596
          .execute();
1✔
597
      deletedCount.val = dslContext.deleteFrom(SECRETS)
1✔
598
          .where(SECRETS.ID.in(ids))
1✔
599
          .execute();
1✔
600
    });
1✔
601
    return deletedCount.val;
1✔
602
  }
603

604
  public static class SecretSeriesDAOFactory implements DAOFactory<SecretSeriesDAO> {
605
    private final DSLContext jooq;
606
    private final DSLContext readonlyJooq;
607
    private final ObjectMapper objectMapper;
608
    private final SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory;
609
    private final RowHmacGenerator rowHmacGenerator;
610

611
    @Inject public SecretSeriesDAOFactory(
612
        DSLContext jooq,
613
        @Readonly DSLContext readonlyJooq,
614
        ObjectMapper objectMapper,
615
        SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory,
616
        RowHmacGenerator rowHmacGenerator) {
1✔
617
      this.jooq = jooq;
1✔
618
      this.readonlyJooq = readonlyJooq;
1✔
619
      this.objectMapper = objectMapper;
1✔
620
      this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
621
      this.rowHmacGenerator = rowHmacGenerator;
1✔
622
    }
1✔
623

624
    @Override public SecretSeriesDAO readwrite() {
625
      return new SecretSeriesDAO(
1✔
626
          jooq,
627
          objectMapper,
628
          secretSeriesMapperFactory.using(jooq),
1✔
629
          rowHmacGenerator);
630
    }
631

632
    @Override public SecretSeriesDAO readonly() {
633
      return new SecretSeriesDAO(
×
634
          readonlyJooq,
635
          objectMapper,
636
          secretSeriesMapperFactory.using(readonlyJooq),
×
637
          rowHmacGenerator);
638
    }
639

640
    @Override public SecretSeriesDAO using(Configuration configuration) {
641
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
642
      return new SecretSeriesDAO(
1✔
643
          dslContext,
644
          objectMapper,
645
          secretSeriesMapperFactory.using(dslContext),
1✔
646
          rowHmacGenerator);
647
    }
648
  }
649

650
  // create a new name for the deleted secret, so that deleted secret names can be reused, while
651
  // still having a unique constraint on the name field in the DB
652
  private static String transformNameForDeletion(String name) {
653
    long now = OffsetDateTime.now().toEpochSecond();
1✔
654
    return String.format(".%s.deleted.%d.%s", name, now, UUID.randomUUID());
1✔
655
  }
656

657
  private String computeRowHmac(long secretSeriesId, String secretSeriesName) {
658
    return rowHmacGenerator.computeRowHmac(
1✔
659
        SECRETS.getName(),
1✔
660
        List.of(
1✔
661
            secretSeriesName,
662
            secretSeriesId));
1✔
663
  }
664

665
  private String computeRowHmacForDeletedSecret(
666
      long secretSeriesId,
667
      String secretSeriesName
668
  ) {
669
    return rowHmacGenerator.computeRowHmac(
1✔
670
        DELETED_SECRETS.getName(),
1✔
671
        List.of(
1✔
672
            secretSeriesName,
673
            secretSeriesId));
1✔
674
  }
675
}
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