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

square / keywhiz / 3850256516

pending completion
3850256516

push

github

GitHub
Adding bookkeeping to keep secret series and secret content expiry in… (#1177)

29 of 29 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.Record;
35
import org.jooq.SelectQuery;
36
import org.jooq.Table;
37
import org.jooq.impl.DSL;
38

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

49
import static com.google.common.base.Preconditions.checkNotNull;
50
import static keywhiz.jooq.tables.Accessgrants.ACCESSGRANTS;
51
import static keywhiz.jooq.tables.Groups.GROUPS;
52
import static keywhiz.jooq.tables.Secrets.SECRETS;
53
import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT;
54

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

64
  private SecretSeriesDAO(
65
      DSLContext dslContext,
66
      ObjectMapper mapper,
67
      SecretSeriesMapper secretSeriesMapper,
68
      RowHmacGenerator rowHmacGenerator) {
1✔
69
    this.dslContext = dslContext;
1✔
70
    this.mapper = mapper;
1✔
71
    this.secretSeriesMapper = secretSeriesMapper;
1✔
72
    this.rowHmacGenerator = rowHmacGenerator;
1✔
73
  }
1✔
74

75
  public boolean secretSeriesExists(String name) {
76
    return dslContext.fetchExists(SECRETS, SECRETS.NAME.eq(name));
1✔
77
  }
78

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

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

111
    String rowHmac = computeRowHmac(id, name);
1✔
112

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

135
    return r.getId();
1✔
136
  }
137

138
  void updateSecretSeries(
139
      long secretId,
140
      String name,
141
      Long ownerId,
142
      String creator,
143
      String description,
144
      @Nullable String type,
145
      @Nullable Map<String, String> generationOptions,
146
      long now) {
147

148
    if (generationOptions == null) {
1✔
149
      generationOptions = ImmutableMap.of();
1✔
150
    }
151

152
    try {
153
      String rowHmac = computeRowHmac(secretId, name);
1✔
154

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

172
  public int setExpiration(long secretContentId, Instant expiration) {
173
    return dslContext.transactionResult(configuration -> {
1✔
174
      SecretsContentRecord content = dslContext.select(SECRETS_CONTENT.EXPIRY)
1✔
175
          .from(SECRETS_CONTENT)
1✔
176
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
177
          .forUpdate()
1✔
178
          .fetchOneInto(SecretsContentRecord.class);
1✔
179

180
      if (content == null) {
1✔
181
        return 0;
×
182
      }
183

184
      Long currentExpiry = content.getExpiry();
1✔
185
      long epochSeconds = expiration.getEpochSecond();
1✔
186

187
      Long updatedExpiry = (currentExpiry == null || currentExpiry == 0)
1✔
188
          ? epochSeconds
1✔
189
          : Math.min(currentExpiry, epochSeconds);
1✔
190

191
      int contentsUpdated = dslContext.update(SECRETS_CONTENT)
1✔
192
          .set(SECRETS_CONTENT.EXPIRY, updatedExpiry)
1✔
193
          .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
194
          .execute();
1✔
195

196
      int secretsUpdated = dslContext.update(SECRETS)
1✔
197
          .set(SECRETS.EXPIRY, updatedExpiry)
1✔
198
          .where(SECRETS.CURRENT.eq(secretContentId))
1✔
199
          .execute();
1✔
200

201
      return contentsUpdated + secretsUpdated;
1✔
202
    });
203
  }
204

205
  public int setRowHmacByName(String secretName, String hmac) {
206
    return dslContext.update(SECRETS)
1✔
207
        .set(SECRETS.ROW_HMAC, hmac)
1✔
208
        .where(SECRETS.NAME.eq(secretName))
1✔
209
        .execute();
1✔
210
  }
211

212
  public int setHmac(long secretContentId, String hmac) {
213
    return dslContext.update(SECRETS_CONTENT)
×
214
        .set(SECRETS_CONTENT.CONTENT_HMAC, hmac)
×
215
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
×
216
        .execute();
×
217
  }
218

219
  public int setCurrentVersion(long secretId, long secretContentId, String updater, long now) {
220
    SecretsContentRecord r = dslContext
1✔
221
        .select(
1✔
222
            SECRETS_CONTENT.SECRETID,
223
            SECRETS_CONTENT.EXPIRY)
224
        .from(SECRETS_CONTENT)
1✔
225
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
226
        .fetchOneInto(SECRETS_CONTENT);
1✔
227

228
    if (r == null) {
1✔
229
      throw new BadRequestException(
1✔
230
          String.format("The requested version %d is not a known version of this secret",
1✔
231
              secretContentId));
1✔
232
    }
233

234
    long checkId = r.getSecretid();
1✔
235
    if (checkId != secretId) {
1✔
236
      throw new IllegalStateException(String.format(
1✔
237
          "tried to reset secret with id %d to version %d, but this version is not associated with this secret",
238
          secretId, secretContentId));
1✔
239
    }
240

241
    return dslContext.update(SECRETS)
1✔
242
        .set(SECRETS.CURRENT, secretContentId)
1✔
243
        .set(SECRETS.EXPIRY, r.getExpiry())
1✔
244
        .set(SECRETS.UPDATEDBY, updater)
1✔
245
        .set(SECRETS.UPDATEDAT, now)
1✔
246
        .where(SECRETS.ID.eq(secretId))
1✔
247
        .execute();
1✔
248
  }
249

250
  public Optional<SecretSeries> getSecretSeriesById(long id) {
251
    SecretsRecord r = getSecretSeriesRecordById(id);
1✔
252
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
253
  }
254

255
  @VisibleForTesting
256
  SecretsRecord getSecretSeriesRecordById(long id) {
257
    return dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
258
  }
259

260
  public Optional<SecretSeries> getDeletedSecretSeriesById(long id) {
261
    SecretsRecord r =
1✔
262
        dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNull()));
1✔
263
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
264
  }
265

266
  public Optional<SecretSeries> getSecretSeriesByName(String name) {
267
    SecretsRecord r =
1✔
268
        dslContext.fetchOne(SECRETS, SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()));
1✔
269
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
270
  }
271

272
  public List<SecretSeries> getSecretSeriesByDeletedName(String name) {
273
    String lookup = "." + name + ".%";
1✔
274
    return dslContext.fetch(SECRETS, SECRETS.NAME.like(lookup).and(SECRETS.CURRENT.isNull())).map(secretSeriesMapper::map);
1✔
275
  }
276

277
  public List<SecretSeries> getMultipleSecretSeriesByName(List<String> names) {
278
    return dslContext.fetch(SECRETS, SECRETS.NAME.in(names).and(SECRETS.CURRENT.isNotNull())).map(secretSeriesMapper::map);
1✔
279
  }
280

281
  public ImmutableList<SecretSeries> getSecretSeries(@Nullable Long expireMaxTime,
282
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
283
      @Nullable Integer limit) {
284

285
    Table<SecretsContentRecord> secretsContentTable = SECRETS_CONTENT;
1✔
286
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
287
      // Force this join to use the index on the secrets_content.expiry
288
      // field. The optimizer may fail to use this index when the SELECT
289
      // examines a large number of rows, causing significant performance
290
      // degradation.
291
      secretsContentTable = secretsContentTable.useIndexForJoin("secrets_content_expiry");
1✔
292
    }
293

294
    SelectQuery<Record> select = dslContext
1✔
295
          .select(SECRETS.fields())
1✔
296
          .from(SECRETS)
1✔
297
          .join(secretsContentTable)
1✔
298
          .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
299
          .where(SECRETS.CURRENT.isNotNull())
1✔
300
          .getQuery();
1✔
301
    select.addOrderBy(SECRETS_CONTENT.EXPIRY.asc(), SECRETS.NAME.asc());
1✔
302

303
    // Set an upper bound on expiration dates
304
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
305
      // Set a lower bound of "now" on the expiration only if it isn't configured separately
306
      if (expireMinTime == null || expireMinTime == 0) {
1✔
307
        long now = System.currentTimeMillis() / 1000L;
1✔
308
        select.addConditions(SECRETS_CONTENT.EXPIRY.greaterOrEqual(now));
1✔
309
      }
310
      select.addConditions(SECRETS_CONTENT.EXPIRY.lessThan(expireMaxTime));
1✔
311
    }
312

313
    if (expireMinTime != null && expireMinTime > 0) {
1✔
314
      // set a lower bound on expiration dates, using the secret name as a tiebreaker
315
      select.addConditions(SECRETS_CONTENT.EXPIRY.greaterThan(expireMinTime)
1✔
316
          .or(SECRETS_CONTENT.EXPIRY.eq(expireMinTime)
1✔
317
              .and(SECRETS.NAME.greaterOrEqual(minName))));
1✔
318
    }
319

320
    if (group != null) {
1✔
321
      select.addJoin(ACCESSGRANTS, SECRETS.ID.eq(ACCESSGRANTS.SECRETID));
1✔
322
      select.addJoin(GROUPS, GROUPS.ID.eq(ACCESSGRANTS.GROUPID));
1✔
323
      select.addConditions(GROUPS.NAME.eq(group.getName()));
1✔
324
    }
325

326
    if (limit != null && limit >= 0) {
1✔
327
      select.addLimit(limit);
1✔
328
    }
329

330
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
331
    return ImmutableList.copyOf(r);
1✔
332
  }
333

334
  public ImmutableList<SecretSeries> getSecretSeriesBatched(int idx, int num, boolean newestFirst) {
335
    SelectQuery<Record> select = dslContext
1✔
336
        .select()
1✔
337
        .from(SECRETS)
1✔
338
        .join(SECRETS_CONTENT)
1✔
339
        .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
340
        .where(SECRETS.CURRENT.isNotNull())
1✔
341
        .getQuery();
1✔
342
    if (newestFirst) {
1✔
343
      select.addOrderBy(SECRETS.CREATEDAT.desc());
1✔
344
    } else {
345
      select.addOrderBy(SECRETS.CREATEDAT.asc());
1✔
346
    }
347
    select.addLimit(idx, num);
1✔
348

349
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
350
    return ImmutableList.copyOf(r);
1✔
351
  }
352

353
  public void hardDeleteSecretSeriesByName(String name) {
354
    dslContext.transaction(configuration -> {
1✔
355
      DSLContext dslContext = DSL.using(configuration);
1✔
356

357
      SecretsRecord record = dslContext.select()
1✔
358
          .from(SECRETS)
1✔
359
          .where(SECRETS.NAME.eq(name))
1✔
360
          .forUpdate()
1✔
361
          .fetchOneInto(SECRETS);
1✔
362

363
      hardDeleteSecretSeries(dslContext, record);
1✔
364
    });
1✔
365
  }
1✔
366

367
  public void hardDeleteSecretSeriesById(Long id) {
368
    dslContext.transaction(configuration -> {
×
369
      DSLContext dslContext = DSL.using(configuration);
×
370

371
      SecretsRecord record = dslContext.select()
×
372
          .from(SECRETS)
×
373
          .where(SECRETS.ID.eq(id))
×
374
          .forUpdate()
×
375
          .fetchOneInto(SECRETS);
×
376

377
      hardDeleteSecretSeries(dslContext, record);
×
378
    });
×
379
  }
×
380

381
  private static void hardDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
382
    if (record == null) {
1✔
383
      return;
×
384
    }
385

386
    dslContext.deleteFrom(SECRETS_CONTENT)
1✔
387
        .where(SECRETS_CONTENT.SECRETID.eq(record.getId()))
1✔
388
        .execute();
1✔
389
    dslContext.deleteFrom(SECRETS)
1✔
390
        .where(SECRETS.ID.eq(record.getId()))
1✔
391
        .execute();
1✔
392
    dslContext.deleteFrom(ACCESSGRANTS)
1✔
393
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
394
        .execute();
1✔
395
  }
1✔
396

397
  public void softDeleteSecretSeriesByName(String name) {
398
    dslContext.transaction(configuration -> {
1✔
399
      // find the record and lock it until this transaction is complete
400
      SecretsRecord record = DSL.using(configuration)
1✔
401
          .select()
1✔
402
          .from(SECRETS)
1✔
403
          .where(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()))
1✔
404
          .forUpdate()
1✔
405
          .fetchOneInto(SECRETS);
1✔
406

407
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
408
    });
1✔
409
  }
1✔
410

411
  public void softDeleteSecretSeriesById(long id) {
412
    dslContext.transaction(configuration -> {
1✔
413
      // find the record and lock it until this transaction is complete
414
      SecretsRecord record = DSL.using(configuration)
1✔
415
          .select()
1✔
416
          .from(SECRETS)
1✔
417
          .where(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()))
1✔
418
          .forUpdate()
1✔
419
          .fetchOneInto(SECRETS);
1✔
420

421
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
422
    });
1✔
423
  }
1✔
424

425
  private static void softDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
426
    if (record == null) {
1✔
427
      return;
×
428
    }
429

430
    long now = OffsetDateTime.now().toEpochSecond();
1✔
431

432
    dslContext
1✔
433
        .update(SECRETS)
1✔
434
        .set(SECRETS.NAME, transformNameForDeletion(record.getName()))
1✔
435
        .set(SECRETS.CURRENT, (Long) null)
1✔
436
        .set(SECRETS.UPDATEDAT, now)
1✔
437
        .where(SECRETS.ID.eq(record.getId()))
1✔
438
        .execute();
1✔
439

440
    dslContext
1✔
441
        .delete(ACCESSGRANTS)
1✔
442
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
443
        .execute();
1✔
444
  }
1✔
445

446
  public void renameSecretSeriesById(long secretId, String name, String creator, long now) {
447
    String rowHmac = computeRowHmac(secretId, name);
1✔
448
    dslContext.update(SECRETS)
1✔
449
        .set(SECRETS.NAME, name)
1✔
450
        .set(SECRETS.ROW_HMAC, rowHmac)
1✔
451
        .set(SECRETS.UPDATEDBY, creator)
1✔
452
        .set(SECRETS.UPDATEDAT, now)
1✔
453
        .where(SECRETS.ID.eq(secretId))
1✔
454
        .execute();
1✔
455
  }
1✔
456

457
  /**
458
   * @return the number of deleted secret series
459
   */
460
  public int countDeletedSecretSeries() {
461
    return dslContext.selectCount()
1✔
462
        .from(SECRETS)
1✔
463
        .where(SECRETS.CURRENT.isNull())
1✔
464
        .fetchOne()
1✔
465
        .value1();
1✔
466
  }
467

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

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

496
  public static class SecretSeriesDAOFactory implements DAOFactory<SecretSeriesDAO> {
497
    private final DSLContext jooq;
498
    private final DSLContext readonlyJooq;
499
    private final ObjectMapper objectMapper;
500
    private final SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory;
501
    private final RowHmacGenerator rowHmacGenerator;
502

503
    @Inject public SecretSeriesDAOFactory(
504
        DSLContext jooq,
505
        @Readonly DSLContext readonlyJooq,
506
        ObjectMapper objectMapper,
507
        SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory,
508
        RowHmacGenerator rowHmacGenerator) {
1✔
509
      this.jooq = jooq;
1✔
510
      this.readonlyJooq = readonlyJooq;
1✔
511
      this.objectMapper = objectMapper;
1✔
512
      this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
513
      this.rowHmacGenerator = rowHmacGenerator;
1✔
514
    }
1✔
515

516
    @Override public SecretSeriesDAO readwrite() {
517
      return new SecretSeriesDAO(
1✔
518
          jooq,
519
          objectMapper,
520
          secretSeriesMapperFactory.using(jooq),
1✔
521
          rowHmacGenerator);
522
    }
523

524
    @Override public SecretSeriesDAO readonly() {
525
      return new SecretSeriesDAO(
×
526
          readonlyJooq,
527
          objectMapper,
528
          secretSeriesMapperFactory.using(readonlyJooq),
×
529
          rowHmacGenerator);
530
    }
531

532
    @Override public SecretSeriesDAO using(Configuration configuration) {
533
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
534
      return new SecretSeriesDAO(
1✔
535
          dslContext,
536
          objectMapper,
537
          secretSeriesMapperFactory.using(dslContext),
1✔
538
          rowHmacGenerator);
539
    }
540
  }
541

542
  // create a new name for the deleted secret, so that deleted secret names can be reused, while
543
  // still having a unique constraint on the name field in the DB
544
  private static String transformNameForDeletion(String name) {
545
    long now = OffsetDateTime.now().toEpochSecond();
1✔
546
    return String.format(".%s.deleted.%d.%s", name, now, UUID.randomUUID());
1✔
547
  }
548

549
  private String computeRowHmac(long secretSeriesId, String secretSeriesName) {
550
    return rowHmacGenerator.computeRowHmac(
1✔
551
        SECRETS.getName(),
1✔
552
        List.of(
1✔
553
            secretSeriesName,
554
            secretSeriesId));
1✔
555
  }
556
}
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