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

square / keywhiz / 3751743172

pending completion
3751743172

Pull #1177

github

GitHub
Merge 91f472a94 into c47429875
Pull Request #1177: Adding bookkeeping to keep secret series and secret content expiry in…

30 of 30 new or added lines in 1 file covered. (100.0%)

5202 of 6728 relevant lines covered (77.32%)

0.77 hits per line

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

91.15
/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 keywhiz.api.model.Group;
26
import keywhiz.api.model.SecretSeries;
27
import keywhiz.jooq.tables.records.SecretsContentRecord;
28
import keywhiz.jooq.tables.records.SecretsRecord;
29
import keywhiz.service.config.Readonly;
30
import keywhiz.service.crypto.RowHmacGenerator;
31
import org.joda.time.DateTime;
32
import org.jooq.Configuration;
33
import org.jooq.DSLContext;
34
import org.jooq.Field;
35
import org.jooq.Record;
36
import org.jooq.Record1;
37
import org.jooq.Record2;
38
import org.jooq.SelectQuery;
39
import org.jooq.Table;
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.SecretsContent.SECRETS_CONTENT;
57
import static org.jooq.impl.DSL.decode;
58
import static org.jooq.impl.DSL.least;
59
import static org.jooq.impl.DSL.val;
60

61

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

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

82
  public boolean secretSeriesExists(String name) {
83
    return dslContext.fetchExists(SECRETS, SECRETS.NAME.eq(name));
1✔
84
  }
85

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

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

118
    String rowHmac = computeRowHmac(id, name);
1✔
119

120
    r.setId(id);
1✔
121
    r.setName(name);
1✔
122
    r.setOwner(ownerId);
1✔
123
    r.setDescription(description);
1✔
124
    r.setCreatedby(creator);
1✔
125
    r.setCreatedat(now);
1✔
126
    r.setUpdatedby(creator);
1✔
127
    r.setUpdatedat(now);
1✔
128
    r.setType(type);
1✔
129
    r.setRowHmac(rowHmac);
1✔
130
    if (generationOptions != null) {
1✔
131
      try {
132
        r.setOptions(mapper.writeValueAsString(generationOptions));
1✔
133
      } catch (JsonProcessingException e) {
×
134
        // Serialization of a Map<String, String> can never fail.
135
        throw Throwables.propagate(e);
×
136
      }
1✔
137
    } else {
138
      r.setOptions("{}");
1✔
139
    }
140
    r.store();
1✔
141

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

145
  void updateSecretSeries(
146
      long secretId,
147
      String name,
148
      Long ownerId,
149
      String creator,
150
      String description,
151
      @Nullable String type,
152
      @Nullable Map<String, String> generationOptions,
153
      long now) {
154

155
    if (generationOptions == null) {
1✔
156
      generationOptions = ImmutableMap.of();
1✔
157
    }
158

159
    try {
160
      String rowHmac = computeRowHmac(secretId, name);
1✔
161

162
      dslContext.update(SECRETS)
1✔
163
          .set(SECRETS.NAME, name)
1✔
164
          .set(SECRETS.OWNER, ownerId)
1✔
165
          .set(SECRETS.DESCRIPTION, description)
1✔
166
          .set(SECRETS.UPDATEDBY, creator)
1✔
167
          .set(SECRETS.UPDATEDAT, now)
1✔
168
          .set(SECRETS.TYPE, type)
1✔
169
          .set(SECRETS.OPTIONS, mapper.writeValueAsString(generationOptions))
1✔
170
          .set(SECRETS.ROW_HMAC, rowHmac)
1✔
171
          .where(SECRETS.ID.eq(secretId))
1✔
172
          .execute();
1✔
173
    } catch (JsonProcessingException e) {
×
174
      // Serialization of a Map<String, String> can never fail.
175
      throw Throwables.propagate(e);
×
176
    }
1✔
177
  }
1✔
178

179
  public int setExpiration(long secretContentId, Instant expiration) {
180
    return dslContext.transactionResult(configuration -> {
1✔
181
      SecretsContentRecord content = dslContext.select(SECRETS_CONTENT.EXPIRY)
1✔
182
          .from(SECRETS_CONTENT)
1✔
183
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
184
          .forUpdate()
1✔
185
          .fetchOneInto(SecretsContentRecord.class);
1✔
186

187
      if (content == null) {
1✔
188
        return 0;
×
189
      }
190

191
      Long currentExpiry = content.getExpiry();
1✔
192
      long epochSeconds = expiration.getEpochSecond();
1✔
193

194
      Long updatedExpiry = (currentExpiry == null || currentExpiry == 0)
1✔
195
          ? epochSeconds
1✔
196
          : Math.min(currentExpiry, epochSeconds);
1✔
197

198
      int contentsUpdated = dslContext.update(SECRETS_CONTENT)
1✔
199
          .set(SECRETS_CONTENT.EXPIRY, updatedExpiry)
1✔
200
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
201
          .execute();
1✔
202

203
      int secretsUpdated = dslContext.update(SECRETS)
1✔
204
          .set(SECRETS.EXPIRY, updatedExpiry)
1✔
205
          .where(SECRETS.CURRENT.eq(secretContentId))
1✔
206
          .execute();
1✔
207

208
      return contentsUpdated + secretsUpdated;
1✔
209
    });
210
  }
211

212
  public int setRowHmacByName(String secretName, String hmac) {
213
    return dslContext.update(SECRETS)
1✔
214
        .set(SECRETS.ROW_HMAC, hmac)
1✔
215
        .where(SECRETS.NAME.eq(secretName))
1✔
216
        .execute();
1✔
217
  }
218

219
  public int setHmac(long secretContentId, String hmac) {
220
    return dslContext.update(SECRETS_CONTENT)
×
221
        .set(SECRETS_CONTENT.CONTENT_HMAC, hmac)
×
222
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
×
223
        .execute();
×
224
  }
225

226
  public int setCurrentVersion(long secretId, long secretContentId, String updater, long now) {
227
    SecretsContentRecord r = dslContext
1✔
228
        .select(
1✔
229
            SECRETS_CONTENT.SECRETID,
230
            SECRETS_CONTENT.EXPIRY)
231
        .from(SECRETS_CONTENT)
1✔
232
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
233
        .fetchOneInto(SECRETS_CONTENT);
1✔
234

235
    if (r == null) {
1✔
236
      throw new BadRequestException(
1✔
237
          String.format("The requested version %d is not a known version of this secret",
1✔
238
              secretContentId));
1✔
239
    }
240

241
    long checkId = r.getSecretid();
1✔
242
    if (checkId != secretId) {
1✔
243
      throw new IllegalStateException(String.format(
1✔
244
          "tried to reset secret with id %d to version %d, but this version is not associated with this secret",
245
          secretId, secretContentId));
1✔
246
    }
247

248
    return dslContext.update(SECRETS)
1✔
249
        .set(SECRETS.CURRENT, secretContentId)
1✔
250
        .set(SECRETS.EXPIRY, r.getExpiry())
1✔
251
        .set(SECRETS.UPDATEDBY, updater)
1✔
252
        .set(SECRETS.UPDATEDAT, now)
1✔
253
        .where(SECRETS.ID.eq(secretId))
1✔
254
        .execute();
1✔
255
  }
256

257
  public Optional<SecretSeries> getSecretSeriesById(long id) {
258
    SecretsRecord r = getSecretSeriesRecordById(id);
1✔
259
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
260
  }
261

262
  @VisibleForTesting
263
  SecretsRecord getSecretSeriesRecordById(long id) {
264
    return dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
265
  }
266

267
  public Optional<SecretSeries> getDeletedSecretSeriesById(long id) {
268
    SecretsRecord r =
1✔
269
        dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNull()));
1✔
270
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
271
  }
272

273
  public Optional<SecretSeries> getSecretSeriesByName(String name) {
274
    SecretsRecord r =
1✔
275
        dslContext.fetchOne(SECRETS, SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()));
1✔
276
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
277
  }
278

279
  public List<SecretSeries> getSecretSeriesByDeletedName(String name) {
280
    String lookup = "." + name + ".%";
1✔
281
    return dslContext.fetch(SECRETS, SECRETS.NAME.like(lookup).and(SECRETS.CURRENT.isNull())).map(secretSeriesMapper::map);
1✔
282
  }
283

284
  public List<SecretSeries> getMultipleSecretSeriesByName(List<String> names) {
285
    return dslContext.fetch(SECRETS, SECRETS.NAME.in(names).and(SECRETS.CURRENT.isNotNull())).map(secretSeriesMapper::map);
1✔
286
  }
287

288
  public ImmutableList<SecretSeries> getSecretSeries(@Nullable Long expireMaxTime,
289
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
290
      @Nullable Integer limit) {
291

292
    Table<SecretsContentRecord> secretsContentTable = SECRETS_CONTENT;
1✔
293
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
294
      // Force this join to use the index on the secrets_content.expiry
295
      // field. The optimizer may fail to use this index when the SELECT
296
      // examines a large number of rows, causing significant performance
297
      // degradation.
298
      secretsContentTable = secretsContentTable.useIndexForJoin("secrets_content_expiry");
1✔
299
    }
300

301
    SelectQuery<Record> select = dslContext
1✔
302
          .select(SECRETS.fields())
1✔
303
          .from(SECRETS)
1✔
304
          .join(secretsContentTable)
1✔
305
          .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
306
          .where(SECRETS.CURRENT.isNotNull())
1✔
307
          .getQuery();
1✔
308
    select.addOrderBy(SECRETS_CONTENT.EXPIRY.asc(), SECRETS.NAME.asc());
1✔
309

310
    // Set an upper bound on expiration dates
311
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
312
      // Set a lower bound of "now" on the expiration only if it isn't configured separately
313
      if (expireMinTime == null || expireMinTime == 0) {
1✔
314
        long now = System.currentTimeMillis() / 1000L;
1✔
315
        select.addConditions(SECRETS_CONTENT.EXPIRY.greaterOrEqual(now));
1✔
316
      }
317
      select.addConditions(SECRETS_CONTENT.EXPIRY.lessThan(expireMaxTime));
1✔
318
    }
319

320
    if (expireMinTime != null && expireMinTime > 0) {
1✔
321
      // set a lower bound on expiration dates, using the secret name as a tiebreaker
322
      select.addConditions(SECRETS_CONTENT.EXPIRY.greaterThan(expireMinTime)
1✔
323
          .or(SECRETS_CONTENT.EXPIRY.eq(expireMinTime)
1✔
324
              .and(SECRETS.NAME.greaterOrEqual(minName))));
1✔
325
    }
326

327
    if (group != null) {
1✔
328
      select.addJoin(ACCESSGRANTS, SECRETS.ID.eq(ACCESSGRANTS.SECRETID));
1✔
329
      select.addJoin(GROUPS, GROUPS.ID.eq(ACCESSGRANTS.GROUPID));
1✔
330
      select.addConditions(GROUPS.NAME.eq(group.getName()));
1✔
331
    }
332

333
    if (limit != null && limit >= 0) {
1✔
334
      select.addLimit(limit);
1✔
335
    }
336

337
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
338
    return ImmutableList.copyOf(r);
1✔
339
  }
340

341
  public ImmutableList<SecretSeries> getSecretSeriesBatched(int idx, int num, boolean newestFirst) {
342
    SelectQuery<Record> select = dslContext
1✔
343
        .select()
1✔
344
        .from(SECRETS)
1✔
345
        .join(SECRETS_CONTENT)
1✔
346
        .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
347
        .where(SECRETS.CURRENT.isNotNull())
1✔
348
        .getQuery();
1✔
349
    if (newestFirst) {
1✔
350
      select.addOrderBy(SECRETS.CREATEDAT.desc());
1✔
351
    } else {
352
      select.addOrderBy(SECRETS.CREATEDAT.asc());
1✔
353
    }
354
    select.addLimit(idx, num);
1✔
355

356
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
357
    return ImmutableList.copyOf(r);
1✔
358
  }
359

360
  public void hardDeleteSecretSeriesByName(String name) {
361
    dslContext.transaction(configuration -> {
1✔
362
      DSLContext dslContext = DSL.using(configuration);
1✔
363

364
      SecretsRecord record = dslContext.select()
1✔
365
          .from(SECRETS)
1✔
366
          .where(SECRETS.NAME.eq(name))
1✔
367
          .forUpdate()
1✔
368
          .fetchOneInto(SECRETS);
1✔
369

370
      hardDeleteSecretSeries(dslContext, record);
1✔
371
    });
1✔
372
  }
1✔
373

374
  public void hardDeleteSecretSeriesById(Long id) {
375
    dslContext.transaction(configuration -> {
×
376
      DSLContext dslContext = DSL.using(configuration);
×
377

378
      SecretsRecord record = dslContext.select()
×
379
          .from(SECRETS)
×
380
          .where(SECRETS.ID.eq(id))
×
381
          .forUpdate()
×
382
          .fetchOneInto(SECRETS);
×
383

384
      hardDeleteSecretSeries(dslContext, record);
×
385
    });
×
386
  }
×
387

388
  private static void hardDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
389
    if (record == null) {
1✔
390
      return;
×
391
    }
392

393
    dslContext.deleteFrom(SECRETS_CONTENT)
1✔
394
        .where(SECRETS_CONTENT.SECRETID.eq(record.getId()))
1✔
395
        .execute();
1✔
396
    dslContext.deleteFrom(SECRETS)
1✔
397
        .where(SECRETS.ID.eq(record.getId()))
1✔
398
        .execute();
1✔
399
    dslContext.deleteFrom(ACCESSGRANTS)
1✔
400
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
401
        .execute();
1✔
402
  }
1✔
403

404
  public void softDeleteSecretSeriesByName(String name) {
405
    dslContext.transaction(configuration -> {
1✔
406
      // find the record and lock it until this transaction is complete
407
      SecretsRecord record = DSL.using(configuration)
1✔
408
          .select()
1✔
409
          .from(SECRETS)
1✔
410
          .where(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()))
1✔
411
          .forUpdate()
1✔
412
          .fetchOneInto(SECRETS);
1✔
413

414
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
415
    });
1✔
416
  }
1✔
417

418
  public void softDeleteSecretSeriesById(long id) {
419
    dslContext.transaction(configuration -> {
1✔
420
      // find the record and lock it until this transaction is complete
421
      SecretsRecord record = DSL.using(configuration)
1✔
422
          .select()
1✔
423
          .from(SECRETS)
1✔
424
          .where(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()))
1✔
425
          .forUpdate()
1✔
426
          .fetchOneInto(SECRETS);
1✔
427

428
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
429
    });
1✔
430
  }
1✔
431

432
  private static void softDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
433
    if (record == null) {
1✔
434
      return;
×
435
    }
436

437
    long now = OffsetDateTime.now().toEpochSecond();
1✔
438

439
    dslContext
1✔
440
        .update(SECRETS)
1✔
441
        .set(SECRETS.NAME, transformNameForDeletion(record.getName()))
1✔
442
        .set(SECRETS.CURRENT, (Long) null)
1✔
443
        .set(SECRETS.UPDATEDAT, now)
1✔
444
        .where(SECRETS.ID.eq(record.getId()))
1✔
445
        .execute();
1✔
446

447
    dslContext
1✔
448
        .delete(ACCESSGRANTS)
1✔
449
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
450
        .execute();
1✔
451
  }
1✔
452

453
  public void renameSecretSeriesById(long secretId, String name, String creator, long now) {
454
    String rowHmac = computeRowHmac(secretId, name);
1✔
455
    dslContext.update(SECRETS)
1✔
456
        .set(SECRETS.NAME, name)
1✔
457
        .set(SECRETS.ROW_HMAC, rowHmac)
1✔
458
        .set(SECRETS.UPDATEDBY, creator)
1✔
459
        .set(SECRETS.UPDATEDAT, now)
1✔
460
        .where(SECRETS.ID.eq(secretId))
1✔
461
        .execute();
1✔
462
  }
1✔
463

464
  /**
465
   * @return the number of deleted secret series
466
   */
467
  public int countDeletedSecretSeries() {
468
    return dslContext.selectCount()
1✔
469
        .from(SECRETS)
1✔
470
        .where(SECRETS.CURRENT.isNull())
1✔
471
        .fetchOne()
1✔
472
        .value1();
1✔
473
  }
474

475
  /**
476
   * Identify all secret series which were deleted before the given date.
477
   *
478
   * @param deleteBefore the cutoff date; secrets deleted before this date will be returned
479
   * @return IDs for secret series deleted before this date
480
   */
481
  public List<Long> getIdsForSecretSeriesDeletedBeforeDate(DateTime deleteBefore) {
482
    long deleteBeforeSeconds = deleteBefore.getMillis() / 1000;
1✔
483
    return dslContext.select(SECRETS.ID)
1✔
484
        .from(SECRETS)
1✔
485
        .where(SECRETS.CURRENT.isNull())
1✔
486
        .and(SECRETS.UPDATEDAT.le(deleteBeforeSeconds))
1✔
487
        .fetch(SECRETS.ID);
1✔
488
  }
489

490
  /**
491
   * PERMANENTLY REMOVE database records from `secrets` which have the given list of IDs. Does not
492
   * affect the `secrets_content` table.
493
   *
494
   * @param ids the IDs in the `secrets` table to be PERMANENTLY REMOVED
495
   * @return the number of records which were removed
496
   */
497
  public long dangerPermanentlyRemoveRecordsForGivenIDs(List<Long> ids) {
498
    return dslContext.deleteFrom(SECRETS)
1✔
499
        .where(SECRETS.ID.in(ids))
1✔
500
        .execute();
1✔
501
  }
502

503
  public static class SecretSeriesDAOFactory implements DAOFactory<SecretSeriesDAO> {
504
    private final DSLContext jooq;
505
    private final DSLContext readonlyJooq;
506
    private final ObjectMapper objectMapper;
507
    private final SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory;
508
    private final RowHmacGenerator rowHmacGenerator;
509

510
    @Inject public SecretSeriesDAOFactory(
511
        DSLContext jooq,
512
        @Readonly DSLContext readonlyJooq,
513
        ObjectMapper objectMapper,
514
        SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory,
515
        RowHmacGenerator rowHmacGenerator) {
1✔
516
      this.jooq = jooq;
1✔
517
      this.readonlyJooq = readonlyJooq;
1✔
518
      this.objectMapper = objectMapper;
1✔
519
      this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
520
      this.rowHmacGenerator = rowHmacGenerator;
1✔
521
    }
1✔
522

523
    @Override public SecretSeriesDAO readwrite() {
524
      return new SecretSeriesDAO(
1✔
525
          jooq,
526
          objectMapper,
527
          secretSeriesMapperFactory.using(jooq),
1✔
528
          rowHmacGenerator);
529
    }
530

531
    @Override public SecretSeriesDAO readonly() {
532
      return new SecretSeriesDAO(
×
533
          readonlyJooq,
534
          objectMapper,
535
          secretSeriesMapperFactory.using(readonlyJooq),
×
536
          rowHmacGenerator);
537
    }
538

539
    @Override public SecretSeriesDAO using(Configuration configuration) {
540
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
541
      return new SecretSeriesDAO(
1✔
542
          dslContext,
543
          objectMapper,
544
          secretSeriesMapperFactory.using(dslContext),
1✔
545
          rowHmacGenerator);
546
    }
547
  }
548

549
  // create a new name for the deleted secret, so that deleted secret names can be reused, while
550
  // still having a unique constraint on the name field in the DB
551
  private static String transformNameForDeletion(String name) {
552
    long now = OffsetDateTime.now().toEpochSecond();
1✔
553
    return String.format(".%s.deleted.%d.%s", name, now, UUID.randomUUID());
1✔
554
  }
555

556
  private String computeRowHmac(long secretSeriesId, String secretSeriesName) {
557
    return rowHmacGenerator.computeRowHmac(
1✔
558
        SECRETS.getName(),
1✔
559
        List.of(
1✔
560
            secretSeriesName,
561
            secretSeriesId));
1✔
562
  }
563
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc