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

square / keywhiz / 5324813856

pending completion
5324813856

push

github

web-flow
Bump mockito-core from 5.2.0 to 5.4.0 (#1224)

Bumps [mockito-core](https://github.com/mockito/mockito) from 5.2.0 to 5.4.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.2.0...v5.4.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

5401 of 7193 relevant lines covered (75.09%)

0.87 hits per line

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

94.01
/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.Arrays;
26
import java.util.Set;
27
import java.util.stream.Collectors;
28
import java.util.stream.Stream;
29
import keywhiz.api.model.Group;
30
import keywhiz.api.model.SecretSeries;
31
import keywhiz.jooq.tables.Groups;
32
import keywhiz.jooq.tables.records.GroupsRecord;
33
import keywhiz.jooq.tables.records.SecretsContentRecord;
34
import keywhiz.jooq.tables.records.SecretsRecord;
35
import keywhiz.model.SecretsOrDeletedSecretsRecord;
36
import keywhiz.service.config.Readonly;
37
import keywhiz.service.crypto.RowHmacGenerator;
38
import org.joda.time.DateTime;
39
import org.jooq.Condition;
40
import org.jooq.Configuration;
41
import org.jooq.DSLContext;
42
import org.jooq.Field;
43
import org.jooq.Record;
44
import org.jooq.SelectOnConditionStep;
45
import org.jooq.SelectQuery;
46
import org.jooq.Table;
47
import org.jooq.impl.DSL;
48

49
import javax.annotation.Nullable;
50
import javax.inject.Inject;
51
import javax.ws.rs.BadRequestException;
52
import java.time.Instant;
53
import java.time.OffsetDateTime;
54
import java.util.List;
55
import java.util.Map;
56
import java.util.Optional;
57
import java.util.UUID;
58

59
import static com.google.common.base.Preconditions.checkNotNull;
60
import static keywhiz.jooq.tables.Accessgrants.ACCESSGRANTS;
61
import static keywhiz.jooq.tables.DeletedAccessgrants.DELETED_ACCESSGRANTS;
62
import static keywhiz.jooq.tables.Groups.GROUPS;
63
import static keywhiz.jooq.tables.Secrets.SECRETS;
64
import static keywhiz.jooq.tables.DeletedSecrets.DELETED_SECRETS;
65
import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT;
66
import static org.jooq.impl.DSL.select;
67

68
/**
69
 * Interacts with 'secrets' table and actions on {@link SecretSeries} entities.
70
 */
71
public class SecretSeriesDAO {
72
  private static final Groups SECRET_OWNERS = GROUPS.as("owners");
1✔
73

74
  private final DSLContext dslContext;
75
  private final ObjectMapper mapper;
76
  private final SecretSeriesMapper secretSeriesMapper;
77
  private final RowHmacGenerator rowHmacGenerator;
78

79
  private SecretSeriesDAO(
80
      DSLContext dslContext,
81
      ObjectMapper mapper,
82
      SecretSeriesMapper secretSeriesMapper,
83
      RowHmacGenerator rowHmacGenerator) {
1✔
84
    this.dslContext = dslContext;
1✔
85
    this.mapper = mapper;
1✔
86
    this.secretSeriesMapper = secretSeriesMapper;
1✔
87
    this.rowHmacGenerator = rowHmacGenerator;
1✔
88
  }
1✔
89

90
  public boolean secretSeriesExists(String name) {
91
    return dslContext.fetchExists(SECRETS, SECRETS.NAME.eq(name));
1✔
92
  }
93

94
  long createSecretSeries(
95
      String name,
96
      Long ownerId,
97
      String creator,
98
      String description,
99
      @Nullable String type,
100
      @Nullable Map<String, String> generationOptions,
101
      long now) {
102
    long generatedId = rowHmacGenerator.getNextLongSecure();
1✔
103
    return createSecretSeries(
1✔
104
        generatedId,
105
        name,
106
        ownerId,
107
        creator,
108
        description,
109
        type,
110
        generationOptions,
111
        now);
112
  }
113

114
  @VisibleForTesting
115
  long createSecretSeries(
116
      long id,
117
      String name,
118
      Long ownerId,
119
      String creator,
120
      String description,
121
      @Nullable String type,
122
      @Nullable Map<String, String> generationOptions,
123
      long now) {
124
    SecretsRecord r = dslContext.newRecord(SECRETS);
1✔
125

126
    String rowHmac = computeRowHmac(id, name);
1✔
127

128
    r.setId(id);
1✔
129
    r.setName(name);
1✔
130
    r.setOwner(ownerId);
1✔
131
    r.setDescription(description);
1✔
132
    r.setCreatedby(creator);
1✔
133
    r.setCreatedat(now);
1✔
134
    r.setUpdatedby(creator);
1✔
135
    r.setUpdatedat(now);
1✔
136
    r.setType(type);
1✔
137
    r.setRowHmac(rowHmac);
1✔
138
    r.setOptions(getOptionsField(generationOptions));
1✔
139

140
    r.store();
1✔
141

142
    return r.getId();
1✔
143
  }
144

145
  private String getOptionsField(
146
      @Nullable Map<String, String> generationOptions
147
  ) {
148
    if (generationOptions == null) {
1✔
149
      return "{}";
1✔
150
    }
151
    try {
152
      return mapper.writeValueAsString(generationOptions);
1✔
153
    } catch (JsonProcessingException e) {
×
154
      // Serialization of a Map<String, String> should never fail.
155
      throw Throwables.propagate(e);
×
156
    }
157
  }
158

159
  void updateSecretSeries(
160
      long secretId,
161
      String name,
162
      Long ownerId,
163
      String creator,
164
      String description,
165
      @Nullable String type,
166
      @Nullable Map<String, String> generationOptions,
167
      long now) {
168

169
    if (generationOptions == null) {
1✔
170
      generationOptions = ImmutableMap.of();
1✔
171
    }
172

173
    try {
174
      String rowHmac = computeRowHmac(secretId, name);
1✔
175

176
      dslContext.update(SECRETS)
1✔
177
          .set(SECRETS.NAME, name)
1✔
178
          .set(SECRETS.OWNER, ownerId)
1✔
179
          .set(SECRETS.DESCRIPTION, description)
1✔
180
          .set(SECRETS.UPDATEDBY, creator)
1✔
181
          .set(SECRETS.UPDATEDAT, now)
1✔
182
          .set(SECRETS.TYPE, type)
1✔
183
          .set(SECRETS.OPTIONS, mapper.writeValueAsString(generationOptions))
1✔
184
          .set(SECRETS.ROW_HMAC, rowHmac)
1✔
185
          .where(SECRETS.ID.eq(secretId))
1✔
186
          .execute();
1✔
187
    } catch (JsonProcessingException e) {
×
188
      // Serialization of a Map<String, String> can never fail.
189
      throw Throwables.propagate(e);
×
190
    }
1✔
191
  }
1✔
192

193
  public int setExpiration(long secretContentId, Instant expiration) {
194
    return dslContext.transactionResult(configuration -> {
1✔
195
      SecretsContentRecord content = dslContext.select(SECRETS_CONTENT.EXPIRY)
1✔
196
          .from(SECRETS_CONTENT)
1✔
197
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
198
          .forUpdate()
1✔
199
          .fetchOneInto(SecretsContentRecord.class);
1✔
200

201
      if (content == null) {
1✔
202
        return 0;
×
203
      }
204

205
      Long currentExpiry = content.getExpiry();
1✔
206
      long epochSeconds = expiration.getEpochSecond();
1✔
207

208
      Long updatedExpiry = (currentExpiry == null || currentExpiry == 0)
1✔
209
          ? epochSeconds
1✔
210
          : Math.min(currentExpiry, epochSeconds);
1✔
211

212
      int contentsUpdated = dslContext.update(SECRETS_CONTENT)
1✔
213
          .set(SECRETS_CONTENT.EXPIRY, updatedExpiry)
1✔
214
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
215
          .execute();
1✔
216

217
      int secretsUpdated = dslContext.update(SECRETS)
1✔
218
          .set(SECRETS.EXPIRY, updatedExpiry)
1✔
219
          .where(SECRETS.CURRENT.eq(secretContentId))
1✔
220
          .execute();
1✔
221

222
      return contentsUpdated + secretsUpdated;
1✔
223
    });
224
  }
225

226
  public int setRowHmacByName(String secretName, String hmac) {
227
    return dslContext.update(SECRETS)
1✔
228
        .set(SECRETS.ROW_HMAC, hmac)
1✔
229
        .where(SECRETS.NAME.eq(secretName))
1✔
230
        .execute();
1✔
231
  }
232

233
  public int setHmac(long secretContentId, String hmac) {
234
    return dslContext.update(SECRETS_CONTENT)
×
235
        .set(SECRETS_CONTENT.CONTENT_HMAC, hmac)
×
236
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
×
237
        .execute();
×
238
  }
239

240
  public int setCurrentVersion(long secretId, long secretContentId, String updater, long now) {
241
    SecretsContentRecord r = dslContext
1✔
242
        .select(
1✔
243
            SECRETS_CONTENT.SECRETID,
244
            SECRETS_CONTENT.EXPIRY)
245
        .from(SECRETS_CONTENT)
1✔
246
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
247
        .fetchOneInto(SECRETS_CONTENT);
1✔
248

249
    if (r == null) {
1✔
250
      throw new BadRequestException(
1✔
251
          String.format("The requested version %d is not a known version of this secret",
1✔
252
              secretContentId));
1✔
253
    }
254

255
    long checkId = r.getSecretid();
1✔
256
    if (checkId != secretId) {
1✔
257
      throw new IllegalStateException(String.format(
1✔
258
          "tried to reset secret with id %d to version %d, but this version is not associated with this secret",
259
          secretId, secretContentId));
1✔
260
    }
261

262
    return dslContext.update(SECRETS)
1✔
263
        .set(SECRETS.CURRENT, secretContentId)
1✔
264
        .set(SECRETS.EXPIRY, r.getExpiry())
1✔
265
        .set(SECRETS.UPDATEDBY, updater)
1✔
266
        .set(SECRETS.UPDATEDAT, now)
1✔
267
        .where(SECRETS.ID.eq(secretId))
1✔
268
        .execute();
1✔
269
  }
270

271
  public Optional<SecretSeries> getSecretSeriesById(long id) {
272
    return getSecretSeries(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
273
  }
274

275
  @VisibleForTesting
276
  SecretsRecord getSecretSeriesRecordById(long id) {
277
    return dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
278
  }
279

280
  public Optional<SecretSeries> getDeletedSecretSeriesById(long id) {
281
    Optional<SecretSeries> fromDeletedTable = getDeletedSecretSeriesFromDeletedSecretsTable(id);
1✔
282
    if (fromDeletedTable.isPresent()) {
1✔
283
      return fromDeletedTable;
1✔
284
    }
285
    return getDeletedSecretSeriesFromMainSecretsTable(id);
1✔
286
  }
287

288
  private Optional<SecretSeries> getDeletedSecretSeriesFromMainSecretsTable(long id) {
289
    return getSecretSeries(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNull()));
1✔
290
  }
291

292
  private Optional<SecretSeries> getDeletedSecretSeriesFromDeletedSecretsTable(long id) {
293
    return getSecretSeries(DELETED_SECRETS, DELETED_SECRETS.ID.eq(id));
1✔
294
  }
295

296
  public Optional<SecretSeries> getSecretSeriesByName(String name) {
297
    return getSecretSeries(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()));
1✔
298
  }
299

300
  public List<SecretSeries> getSecretSeriesByDeletedName(String name) {
301
    List<SecretSeries> fromDeletedSecretsTable =
1✔
302
        getDeletedSecretSeriesFromDeletedSecretsTable(name);
1✔
303
    Set<Long> idsFromDeletedSecretsTable =
1✔
304
        fromDeletedSecretsTable.stream().map(SecretSeries::id).collect(Collectors.toSet());
1✔
305
    return Stream.concat(fromDeletedSecretsTable.stream(),
1✔
306
            getDeletedSecretSeriesFromMainSecretsTable(name).stream()
1✔
307
                // If a secret series exists in both tables, only include the copy from `deleted_secrets`
308
                // rather than the copy from `secrets` since it contains more information.
309
                .filter(secretSeries -> !idsFromDeletedSecretsTable.contains(secretSeries.id())))
1✔
310
        .collect(Collectors.toList());
1✔
311
  }
312

313
  private List<SecretSeries> getDeletedSecretSeriesFromMainSecretsTable(String name) {
314
    String lookup = "." + name + ".%";
1✔
315
    return getMultipleSecretSeries(SECRETS.NAME.like(lookup).and(SECRETS.CURRENT.isNull()));
1✔
316
  }
317

318
  private List<SecretSeries> getDeletedSecretSeriesFromDeletedSecretsTable(String name) {
319
    return getMultipleSecretSeries(DELETED_SECRETS, DELETED_SECRETS.NAME.eq(name));
1✔
320
  }
321

322
  public List<SecretSeries> getMultipleSecretSeriesByName(List<String> names) {
323
    return getMultipleSecretSeries(SECRETS.NAME.in(names).and(SECRETS.CURRENT.isNotNull()));
1✔
324
  }
325

326
  SelectQuery<Record> baseSelectQuery() {
327
        return baseSelect().getQuery();
1✔
328
  }
329

330
  private SelectOnConditionStep<Record> baseSelect() {
331
    return baseSelect(SECRETS);
1✔
332
  }
333

334
  private SelectOnConditionStep<Record> baseSelect(Table<? extends SecretsOrDeletedSecretsRecord> table) {
335
    return dslContext
1✔
336
        .select(table.fields())
1✔
337
        .select(SECRET_OWNERS.fields())
1✔
338
        .from(table)
1✔
339
        .leftJoin(SECRET_OWNERS)
1✔
340
        .on(table.field("owner", Long.class).eq(SECRET_OWNERS.ID));
1✔
341
  }
342

343
  List<SecretSeries> getMultipleSecretSeriesFromQuery(SelectQuery<Record> query) {
344
    return query.fetch().map(this::recordToSecretSeries);
1✔
345
  }
346

347
  public Optional<SecretSeries> getSecretSeriesFromQuery(SelectQuery<Record> query) {
348
    return Optional.ofNullable(query.fetchOne()).map(this::recordToSecretSeries);
1✔
349
  }
350

351
  List<SecretSeries> getMultipleSecretSeries(Condition condition) {
352
    return getMultipleSecretSeries(SECRETS, condition);
1✔
353
  }
354

355
  List<SecretSeries> getMultipleSecretSeries(Table<? extends SecretsOrDeletedSecretsRecord> table, Condition condition) {
356
    return baseSelect(table)
1✔
357
        .where(condition)
1✔
358
        .fetch()
1✔
359
        .map(this::recordToSecretSeries);
1✔
360
  }
361

362
  private Optional<SecretSeries> getSecretSeries(Condition condition) {
363
    return getSecretSeries(SECRETS, condition);
1✔
364
  }
365

366
  private Optional<SecretSeries> getSecretSeries(Table<? extends SecretsOrDeletedSecretsRecord> table, Condition condition) {
367
    Record record = baseSelect(table)
1✔
368
        .where(condition)
1✔
369
        .fetchOne();
1✔
370

371
    return Optional.ofNullable(recordToSecretSeries(record));
1✔
372
  }
373

374
  SecretSeries recordToSecretSeries(Record record) {
375
    if (record == null) {
1✔
376
      return null;
1✔
377
    }
378

379
    SecretsRecord secretRecord = record.into(SECRETS);
1✔
380
    GroupsRecord ownerRecord = record.into(SECRET_OWNERS);
1✔
381

382
    throwIfDanglingOwner(secretRecord, ownerRecord);
1✔
383

384
    SecretSeries secretSeries = secretSeriesMapper.map(secretRecord);
1✔
385
    if (secretRecord.getOwner() != null) {
1✔
386
      secretSeries = secretSeries.toBuilder()
1✔
387
          .owner(ownerRecord.getName())
1✔
388
          .build();
1✔
389
    }
390

391
    return secretSeries;
1✔
392
  }
393

394
  private static void throwIfDanglingOwner(SecretsRecord secretRecord, GroupsRecord ownerRecord) {
395
    boolean danglingOwner = secretRecord.getOwner() != null && ownerRecord.getId() == null;
1✔
396
    if (danglingOwner) {
1✔
397
      throw new IllegalStateException(
1✔
398
          String.format("Owner %s for secret %s is missing.",
1✔
399
              secretRecord.getOwner(),
1✔
400
              secretRecord.getName()));
1✔
401
    }
402
  }
1✔
403

404
  public List<String> listExpiringSecretNames(Instant notAfterInclusive) {
405
    List<String> expiringSecretNames = dslContext
1✔
406
        .select()
1✔
407
        .from(SECRETS)
1✔
408
        .where(SECRETS.CURRENT.isNotNull())
1✔
409
        .and(SECRETS.EXPIRY.greaterOrEqual(Instant.now().getEpochSecond()))
1✔
410
        .and(SECRETS.EXPIRY.lessThan(notAfterInclusive.getEpochSecond()))
1✔
411
        .fetch(SECRETS.NAME);
1✔
412
    return ImmutableList.copyOf(expiringSecretNames);
1✔
413
  }
414

415
  public ImmutableList<SecretSeries> getSecretSeries(@Nullable Long expireMaxTime,
416
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
417
      @Nullable Integer limit) {
418

419
    SelectQuery<Record> select = baseSelect()
1✔
420
          .where(SECRETS.CURRENT.isNotNull())
1✔
421
          .getQuery();
1✔
422
    select.addOrderBy(SECRETS.EXPIRY.asc(), SECRETS.NAME.asc());
1✔
423

424
    // Set an upper bound on expiration dates
425
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
426
      // Set a lower bound of "now" on the expiration only if it isn't configured separately
427
      if (expireMinTime == null || expireMinTime == 0) {
1✔
428
        long now = System.currentTimeMillis() / 1000L;
1✔
429
        select.addConditions(SECRETS.EXPIRY.greaterOrEqual(now));
1✔
430
      }
431
      select.addConditions(SECRETS.EXPIRY.lessThan(expireMaxTime));
1✔
432
    }
433

434
    if (expireMinTime != null && expireMinTime > 0) {
1✔
435
      // set a lower bound on expiration dates, using the secret name as a tiebreaker
436
      select.addConditions(SECRETS.EXPIRY.greaterThan(expireMinTime)
1✔
437
          .or(SECRETS.EXPIRY.eq(expireMinTime)
1✔
438
              .and(SECRETS.NAME.greaterOrEqual(minName))));
1✔
439
    }
440

441
    if (group != null) {
1✔
442
      select.addJoin(ACCESSGRANTS, SECRETS.ID.eq(ACCESSGRANTS.SECRETID));
1✔
443
      select.addJoin(GROUPS, GROUPS.ID.eq(ACCESSGRANTS.GROUPID));
1✔
444
      select.addConditions(GROUPS.NAME.eq(group.getName()));
1✔
445
    }
446

447
    if (limit != null && limit >= 0) {
1✔
448
      select.addLimit(limit);
1✔
449
    }
450

451
    List<SecretSeries> r = select.fetch().map(this::recordToSecretSeries);
1✔
452
    return ImmutableList.copyOf(r);
1✔
453
  }
454

455
  public ImmutableList<SecretSeries> getSecretSeriesBatched(int idx, int num, boolean newestFirst) {
456
    SelectQuery<Record> select = baseSelect()
1✔
457
        .join(SECRETS_CONTENT)
1✔
458
        .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
459
        .where(SECRETS.CURRENT.isNotNull())
1✔
460
        .getQuery();
1✔
461
    if (newestFirst) {
1✔
462
      select.addOrderBy(SECRETS.CREATEDAT.desc());
1✔
463
    } else {
464
      select.addOrderBy(SECRETS.CREATEDAT.asc());
1✔
465
    }
466
    select.addLimit(idx, num);
1✔
467

468
    List<SecretSeries> r = select.fetch().map(this::recordToSecretSeries);
1✔
469
    return ImmutableList.copyOf(r);
1✔
470
  }
471

472
  public void hardDeleteSecretSeriesByName(String name) {
473
    dslContext.transaction(configuration -> {
1✔
474
      DSLContext dslContext = DSL.using(configuration);
1✔
475

476
      SecretsRecord record = dslContext.select()
1✔
477
          .from(SECRETS)
1✔
478
          .where(SECRETS.NAME.eq(name))
1✔
479
          .forUpdate()
1✔
480
          .fetchOneInto(SECRETS);
1✔
481

482
      hardDeleteSecretSeries(dslContext, record);
1✔
483
    });
1✔
484
  }
1✔
485

486
  public void hardDeleteSecretSeriesById(Long id) {
487
    dslContext.transaction(configuration -> {
×
488
      DSLContext dslContext = DSL.using(configuration);
×
489

490
      SecretsRecord record = dslContext.select()
×
491
          .from(SECRETS)
×
492
          .where(SECRETS.ID.eq(id))
×
493
          .forUpdate()
×
494
          .fetchOneInto(SECRETS);
×
495

496
      hardDeleteSecretSeries(dslContext, record);
×
497
    });
×
498
  }
×
499

500
  private static void hardDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
501
    if (record == null) {
1✔
502
      return;
×
503
    }
504

505
    dslContext.deleteFrom(SECRETS_CONTENT)
1✔
506
        .where(SECRETS_CONTENT.SECRETID.eq(record.getId()))
1✔
507
        .execute();
1✔
508
    dslContext.deleteFrom(SECRETS)
1✔
509
        .where(SECRETS.ID.eq(record.getId()))
1✔
510
        .execute();
1✔
511
    dslContext.deleteFrom(ACCESSGRANTS)
1✔
512
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
513
        .execute();
1✔
514
  }
1✔
515

516
  public void softDeleteSecretSeriesByName(String name) {
517
    dslContext.transaction(configuration -> {
1✔
518
      // find the record and lock it until this transaction is complete
519
      SecretsRecord record = DSL.using(configuration)
1✔
520
          .select()
1✔
521
          .from(SECRETS)
1✔
522
          .where(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()))
1✔
523
          .forUpdate()
1✔
524
          .fetchOneInto(SECRETS);
1✔
525

526
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
527
    });
1✔
528
  }
1✔
529

530
  public void softDeleteSecretSeriesById(long id) {
531
    dslContext.transaction(configuration -> {
1✔
532
      // find the record and lock it until this transaction is complete
533
      SecretsRecord record = DSL.using(configuration)
1✔
534
          .select()
1✔
535
          .from(SECRETS)
1✔
536
          .where(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()))
1✔
537
          .forUpdate()
1✔
538
          .fetchOneInto(SECRETS);
1✔
539

540
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
541
    });
1✔
542
  }
1✔
543

544
  private static void softDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
545
    if (record == null) {
1✔
546
      return;
×
547
    }
548

549
    long now = OffsetDateTime.now().toEpochSecond();
1✔
550

551
    dslContext
1✔
552
        .insertInto(DELETED_SECRETS)
1✔
553
        .columns(DELETED_SECRETS.fields())
1✔
554
        .select(select(Arrays.stream(DELETED_SECRETS.fields())
1✔
555
            .map(SECRETS::field)
1✔
556
            .collect(Collectors.toList())).from(SECRETS).where(SECRETS.ID.eq(record.getId())))
1✔
557
            .execute();
1✔
558

559
    dslContext
1✔
560
        .update(SECRETS)
1✔
561
        .set(SECRETS.NAME, transformNameForDeletion(record.getName()))
1✔
562
        .set(SECRETS.CURRENT, (Long) null)
1✔
563
        .set(SECRETS.UPDATEDAT, now)
1✔
564
        .where(SECRETS.ID.eq(record.getId()))
1✔
565
        .execute();
1✔
566

567
    List<Field<?>> fieldsToCopy = Arrays.stream(DELETED_ACCESSGRANTS.fields())
1✔
568
        .filter(field -> !field.getName().equals(DELETED_ACCESSGRANTS.ID.getName()))
1✔
569
        .collect(Collectors.toList());
1✔
570

571
    dslContext
1✔
572
        .insertInto(DELETED_ACCESSGRANTS)
1✔
573
        .columns(fieldsToCopy)
1✔
574
        .select(select(
1✔
575
            fieldsToCopy.stream()
1✔
576
                .map(ACCESSGRANTS::field)
1✔
577
                .collect(Collectors.toList()))
1✔
578
            .from(ACCESSGRANTS)
1✔
579
            .where(ACCESSGRANTS.SECRETID.eq(record.getId())))
1✔
580
        .execute();
1✔
581

582
    dslContext
1✔
583
        .delete(ACCESSGRANTS)
1✔
584
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
585
        .execute();
1✔
586
  }
1✔
587

588
  public void undeleteSoftDeletedSecretSeriesById(long id) {
589
    dslContext.transaction(configuration -> {
1✔
590
      undeleteSoftDeletedSecretSeriesById(DSL.using(configuration), id);
1✔
591
    });
1✔
592
  }
1✔
593

594
  private static void undeleteSoftDeletedSecretSeriesById(
595
      DSLContext dslContext,
596
      long id
597
  ) {
598
    long now = OffsetDateTime.now().toEpochSecond();
1✔
599

600
    dslContext
1✔
601
        .update(SECRETS)
1✔
602
        .set(SECRETS.NAME, select(
1✔
603
            DELETED_SECRETS.NAME
604
        ).from(DELETED_SECRETS).where(DELETED_SECRETS.ID.eq(id)))
1✔
605
        .set(SECRETS.CURRENT, select(
1✔
606
            DELETED_SECRETS.CURRENT
607
        ).from(DELETED_SECRETS).where(DELETED_SECRETS.ID.eq(id)))
1✔
608
        .set(SECRETS.UPDATEDAT, now)
1✔
609
        .where(SECRETS.ID.eq(id))
1✔
610
        .execute();
1✔
611

612
    dslContext
1✔
613
        .delete(DELETED_SECRETS)
1✔
614
        .where(DELETED_SECRETS.ID.eq(id))
1✔
615
        .execute();
1✔
616

617
    List<Field<?>> fieldsToCopy = Arrays.stream(ACCESSGRANTS.fields())
1✔
618
        .filter(field -> !field.getName().equals(ACCESSGRANTS.ID.getName()))
1✔
619
        .collect(Collectors.toList());
1✔
620

621
    dslContext
1✔
622
        .insertInto(ACCESSGRANTS)
1✔
623
        .columns(fieldsToCopy)
1✔
624
        .select(select(
1✔
625
            fieldsToCopy.stream()
1✔
626
                .map(DELETED_ACCESSGRANTS::field)
1✔
627
                .collect(Collectors.toList()))
1✔
628
            .from(DELETED_ACCESSGRANTS)
1✔
629
            .where(DELETED_ACCESSGRANTS.SECRETID.eq(id)))
1✔
630
        .execute();
1✔
631

632
    dslContext
1✔
633
        .delete(DELETED_ACCESSGRANTS)
1✔
634
        .where(DELETED_ACCESSGRANTS.SECRETID.eq(id))
1✔
635
        .execute();
1✔
636
  }
1✔
637

638
  public void renameSecretSeriesById(long secretId, String name, String creator, long now) {
639
    String rowHmac = computeRowHmac(secretId, name);
1✔
640
    dslContext.update(SECRETS)
1✔
641
        .set(SECRETS.NAME, name)
1✔
642
        .set(SECRETS.ROW_HMAC, rowHmac)
1✔
643
        .set(SECRETS.UPDATEDBY, creator)
1✔
644
        .set(SECRETS.UPDATEDAT, now)
1✔
645
        .where(SECRETS.ID.eq(secretId))
1✔
646
        .execute();
1✔
647
  }
1✔
648

649
  /**
650
   * @return the number of deleted secret series
651
   */
652
  public int countDeletedSecretSeries() {
653
    return dslContext.selectCount()
1✔
654
        .from(SECRETS)
1✔
655
        .where(SECRETS.CURRENT.isNull())
1✔
656
        .fetchOne()
1✔
657
        .value1();
1✔
658
  }
659

660
  /**
661
   * Identify all secret series which were deleted before the given date.
662
   *
663
   * @param deleteBefore the cutoff date; secrets deleted before this date will be returned
664
   * @return IDs for secret series deleted before this date
665
   */
666
  public List<Long> getIdsForSecretSeriesDeletedBeforeDate(DateTime deleteBefore) {
667
    long deleteBeforeSeconds = deleteBefore.getMillis() / 1000;
1✔
668
    return dslContext.select(SECRETS.ID)
1✔
669
        .from(SECRETS)
1✔
670
        .where(SECRETS.CURRENT.isNull())
1✔
671
        .and(SECRETS.UPDATEDAT.le(deleteBeforeSeconds))
1✔
672
        .fetch(SECRETS.ID);
1✔
673
  }
674

675
  /**
676
   * PERMANENTLY REMOVE database records from `secrets` which have the given list of IDs. Does not
677
   * affect the `secrets_content` table.
678
   *
679
   * @param ids the IDs in the `secrets` table to be PERMANENTLY REMOVED
680
   * @return the number of records which were removed
681
   */
682
  public long dangerPermanentlyRemoveRecordsForGivenIDs(List<Long> ids) {
683
    var deletedCount = new Object(){ long val = 0; };
1✔
684
    dslContext.transaction(configuration -> {
1✔
685
      dslContext.deleteFrom(DELETED_SECRETS)
1✔
686
          .where(DELETED_SECRETS.ID.in(ids))
1✔
687
          .execute();
1✔
688
      deletedCount.val = dslContext.deleteFrom(SECRETS)
1✔
689
          .where(SECRETS.ID.in(ids))
1✔
690
          .execute();
1✔
691
    });
1✔
692
    return deletedCount.val;
1✔
693
  }
694

695
  public static class SecretSeriesDAOFactory implements DAOFactory<SecretSeriesDAO> {
696
    private final DSLContext jooq;
697
    private final DSLContext readonlyJooq;
698
    private final ObjectMapper objectMapper;
699
    private final SecretSeriesMapper secretSeriesMapper;
700
    private final RowHmacGenerator rowHmacGenerator;
701

702
    @Inject public SecretSeriesDAOFactory(
703
        DSLContext jooq,
704
        @Readonly DSLContext readonlyJooq,
705
        ObjectMapper objectMapper,
706
        SecretSeriesMapper secretSeriesMapper,
707
        RowHmacGenerator rowHmacGenerator) {
1✔
708
      this.jooq = jooq;
1✔
709
      this.readonlyJooq = readonlyJooq;
1✔
710
      this.objectMapper = objectMapper;
1✔
711
      this.secretSeriesMapper = secretSeriesMapper;
1✔
712
      this.rowHmacGenerator = rowHmacGenerator;
1✔
713
    }
1✔
714

715
    @Override public SecretSeriesDAO readwrite() {
716
      return new SecretSeriesDAO(
1✔
717
          jooq,
718
          objectMapper,
719
          secretSeriesMapper,
720
          rowHmacGenerator);
721
    }
722

723
    @Override public SecretSeriesDAO readonly() {
724
      return new SecretSeriesDAO(
×
725
          readonlyJooq,
726
          objectMapper,
727
          secretSeriesMapper,
728
          rowHmacGenerator);
729
    }
730

731
    @Override public SecretSeriesDAO using(Configuration configuration) {
732
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
733
      return new SecretSeriesDAO(
1✔
734
          dslContext,
735
          objectMapper,
736
          secretSeriesMapper,
737
          rowHmacGenerator);
738
    }
739
  }
740

741
  // create a new name for the deleted secret, so that deleted secret names can be reused, while
742
  // still having a unique constraint on the name field in the DB
743
  private static String transformNameForDeletion(String name) {
744
    long now = OffsetDateTime.now().toEpochSecond();
1✔
745
    return String.format(".%s.deleted.%d.%s", name, now, UUID.randomUUID());
1✔
746
  }
747

748
  private String computeRowHmac(long secretSeriesId, String secretSeriesName) {
749
    return rowHmacGenerator.computeRowHmac(
1✔
750
        SECRETS.getName(),
1✔
751
        List.of(
1✔
752
            secretSeriesName,
753
            secretSeriesId));
1✔
754
  }
755
}
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