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

square / keywhiz / 3733400237

pending completion
3733400237

push

github

GitHub
Regenerating database model to include new secrets expiry column and index (#1173)

5186 of 6711 relevant lines covered (77.28%)

0.77 hits per line

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

90.95
/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.SelectQuery;
38
import org.jooq.Table;
39
import org.jooq.impl.DSL;
40

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

51
import static com.google.common.base.Preconditions.checkNotNull;
52
import static keywhiz.jooq.tables.Accessgrants.ACCESSGRANTS;
53
import static keywhiz.jooq.tables.Groups.GROUPS;
54
import static keywhiz.jooq.tables.Secrets.SECRETS;
55
import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT;
56
import static org.jooq.impl.DSL.decode;
57
import static org.jooq.impl.DSL.least;
58
import static org.jooq.impl.DSL.val;
59

60

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

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

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

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

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

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

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

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

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

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

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

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

178
  public int setExpiration(long secretContentId, Instant expiration) {
179
    Field<Long> minExpiration = decode()
1✔
180
        .when(SECRETS_CONTENT.EXPIRY.eq(0L), val(expiration.getEpochSecond()))
1✔
181
        .otherwise(least(SECRETS_CONTENT.EXPIRY, val(expiration.getEpochSecond())));
1✔
182

183
    return dslContext.update(SECRETS_CONTENT)
1✔
184
        .set(SECRETS_CONTENT.EXPIRY, minExpiration)
1✔
185
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
186
        .execute();
1✔
187
  }
188

189
  public int setRowHmacByName(String secretName, String hmac) {
190
    return dslContext.update(SECRETS)
1✔
191
        .set(SECRETS.ROW_HMAC, hmac)
1✔
192
        .where(SECRETS.NAME.eq(secretName))
1✔
193
        .execute();
1✔
194
  }
195

196
  public int setHmac(long secretContentId, String hmac) {
197
    return dslContext.update(SECRETS_CONTENT)
×
198
        .set(SECRETS_CONTENT.CONTENT_HMAC, hmac)
×
199
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
×
200
        .execute();
×
201
  }
202

203
  public int setCurrentVersion(long secretId, long secretContentId, String updater, long now) {
204
    long checkId;
205
    Record1<Long> r = dslContext.select(SECRETS_CONTENT.SECRETID)
1✔
206
        .from(SECRETS_CONTENT)
1✔
207
        .where(SECRETS_CONTENT.ID.eq(secretContentId))
1✔
208
        .fetchOne();
1✔
209
    if (r == null) {
1✔
210
      throw new BadRequestException(
1✔
211
          String.format("The requested version %d is not a known version of this secret",
1✔
212
              secretContentId));
1✔
213
    }
214

215
    checkId = r.value1();
1✔
216
    if (checkId != secretId) {
1✔
217
      throw new IllegalStateException(String.format(
1✔
218
          "tried to reset secret with id %d to version %d, but this version is not associated with this secret",
219
          secretId, secretContentId));
1✔
220
    }
221

222
    return dslContext.update(SECRETS)
1✔
223
        .set(SECRETS.CURRENT, secretContentId)
1✔
224
        .set(SECRETS.UPDATEDBY, updater)
1✔
225
        .set(SECRETS.UPDATEDAT, now)
1✔
226
        .where(SECRETS.ID.eq(secretId))
1✔
227
        .execute();
1✔
228
  }
229

230
  public Optional<SecretSeries> getSecretSeriesById(long id) {
231
    SecretsRecord r =
1✔
232
        dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()));
1✔
233
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
234
  }
235

236
  public Optional<SecretSeries> getDeletedSecretSeriesById(long id) {
237
    SecretsRecord r =
1✔
238
        dslContext.fetchOne(SECRETS, SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNull()));
1✔
239
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
240
  }
241

242
  public Optional<SecretSeries> getSecretSeriesByName(String name) {
243
    SecretsRecord r =
1✔
244
        dslContext.fetchOne(SECRETS, SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()));
1✔
245
    return Optional.ofNullable(r).map(secretSeriesMapper::map);
1✔
246
  }
247

248
  public List<SecretSeries> getSecretSeriesByDeletedName(String name) {
249
    String lookup = "." + name + ".%";
1✔
250
    return dslContext.fetch(SECRETS, SECRETS.NAME.like(lookup).and(SECRETS.CURRENT.isNull())).map(secretSeriesMapper::map);
1✔
251
  }
252

253
  public List<SecretSeries> getMultipleSecretSeriesByName(List<String> names) {
254
    return dslContext.fetch(SECRETS, SECRETS.NAME.in(names).and(SECRETS.CURRENT.isNotNull())).map(secretSeriesMapper::map);
1✔
255
  }
256

257
  public ImmutableList<SecretSeries> getSecretSeries(@Nullable Long expireMaxTime,
258
      @Nullable Group group, @Nullable Long expireMinTime, @Nullable String minName,
259
      @Nullable Integer limit) {
260

261
    Table<SecretsContentRecord> secretsContentTable = SECRETS_CONTENT;
1✔
262
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
263
      // Force this join to use the index on the secrets_content.expiry
264
      // field. The optimizer may fail to use this index when the SELECT
265
      // examines a large number of rows, causing significant performance
266
      // degradation.
267
      secretsContentTable = secretsContentTable.useIndexForJoin("secrets_content_expiry");
1✔
268
    }
269

270
    SelectQuery<Record> select = dslContext
1✔
271
          .select(SECRETS.fields())
1✔
272
          .from(SECRETS)
1✔
273
          .join(secretsContentTable)
1✔
274
          .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
275
          .where(SECRETS.CURRENT.isNotNull())
1✔
276
          .getQuery();
1✔
277
    select.addOrderBy(SECRETS_CONTENT.EXPIRY.asc(), SECRETS.NAME.asc());
1✔
278

279
    // Set an upper bound on expiration dates
280
    if (expireMaxTime != null && expireMaxTime > 0) {
1✔
281
      // Set a lower bound of "now" on the expiration only if it isn't configured separately
282
      if (expireMinTime == null || expireMinTime == 0) {
1✔
283
        long now = System.currentTimeMillis() / 1000L;
1✔
284
        select.addConditions(SECRETS_CONTENT.EXPIRY.greaterOrEqual(now));
1✔
285
      }
286
      select.addConditions(SECRETS_CONTENT.EXPIRY.lessThan(expireMaxTime));
1✔
287
    }
288

289
    if (expireMinTime != null && expireMinTime > 0) {
1✔
290
      // set a lower bound on expiration dates, using the secret name as a tiebreaker
291
      select.addConditions(SECRETS_CONTENT.EXPIRY.greaterThan(expireMinTime)
1✔
292
          .or(SECRETS_CONTENT.EXPIRY.eq(expireMinTime)
1✔
293
              .and(SECRETS.NAME.greaterOrEqual(minName))));
1✔
294
    }
295

296
    if (group != null) {
1✔
297
      select.addJoin(ACCESSGRANTS, SECRETS.ID.eq(ACCESSGRANTS.SECRETID));
1✔
298
      select.addJoin(GROUPS, GROUPS.ID.eq(ACCESSGRANTS.GROUPID));
1✔
299
      select.addConditions(GROUPS.NAME.eq(group.getName()));
1✔
300
    }
301

302
    if (limit != null && limit >= 0) {
1✔
303
      select.addLimit(limit);
1✔
304
    }
305

306
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
307
    return ImmutableList.copyOf(r);
1✔
308
  }
309

310
  public ImmutableList<SecretSeries> getSecretSeriesBatched(int idx, int num, boolean newestFirst) {
311
    SelectQuery<Record> select = dslContext
1✔
312
        .select()
1✔
313
        .from(SECRETS)
1✔
314
        .join(SECRETS_CONTENT)
1✔
315
        .on(SECRETS.CURRENT.equal(SECRETS_CONTENT.ID))
1✔
316
        .where(SECRETS.CURRENT.isNotNull())
1✔
317
        .getQuery();
1✔
318
    if (newestFirst) {
1✔
319
      select.addOrderBy(SECRETS.CREATEDAT.desc());
1✔
320
    } else {
321
      select.addOrderBy(SECRETS.CREATEDAT.asc());
1✔
322
    }
323
    select.addLimit(idx, num);
1✔
324

325
    List<SecretSeries> r = select.fetchInto(SECRETS).map(secretSeriesMapper);
1✔
326
    return ImmutableList.copyOf(r);
1✔
327
  }
328

329
  public void hardDeleteSecretSeriesByName(String name) {
330
    dslContext.transaction(configuration -> {
1✔
331
      DSLContext dslContext = DSL.using(configuration);
1✔
332

333
      SecretsRecord record = dslContext.select()
1✔
334
          .from(SECRETS)
1✔
335
          .where(SECRETS.NAME.eq(name))
1✔
336
          .forUpdate()
1✔
337
          .fetchOneInto(SECRETS);
1✔
338

339
      hardDeleteSecretSeries(dslContext, record);
1✔
340
    });
1✔
341
  }
1✔
342

343
  public void hardDeleteSecretSeriesById(Long id) {
344
    dslContext.transaction(configuration -> {
×
345
      DSLContext dslContext = DSL.using(configuration);
×
346

347
      SecretsRecord record = dslContext.select()
×
348
          .from(SECRETS)
×
349
          .where(SECRETS.ID.eq(id))
×
350
          .forUpdate()
×
351
          .fetchOneInto(SECRETS);
×
352

353
      hardDeleteSecretSeries(dslContext, record);
×
354
    });
×
355
  }
×
356

357
  private static void hardDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
358
    if (record == null) {
1✔
359
      return;
×
360
    }
361

362
    dslContext.deleteFrom(SECRETS_CONTENT)
1✔
363
        .where(SECRETS_CONTENT.SECRETID.eq(record.getId()))
1✔
364
        .execute();
1✔
365
    dslContext.deleteFrom(SECRETS)
1✔
366
        .where(SECRETS.ID.eq(record.getId()))
1✔
367
        .execute();
1✔
368
    dslContext.deleteFrom(ACCESSGRANTS)
1✔
369
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
370
        .execute();
1✔
371
  }
1✔
372

373
  public void softDeleteSecretSeriesByName(String name) {
374
    dslContext.transaction(configuration -> {
1✔
375
      // find the record and lock it until this transaction is complete
376
      SecretsRecord record = DSL.using(configuration)
1✔
377
          .select()
1✔
378
          .from(SECRETS)
1✔
379
          .where(SECRETS.NAME.eq(name).and(SECRETS.CURRENT.isNotNull()))
1✔
380
          .forUpdate()
1✔
381
          .fetchOneInto(SECRETS);
1✔
382

383
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
384
    });
1✔
385
  }
1✔
386

387
  public void softDeleteSecretSeriesById(long id) {
388
    dslContext.transaction(configuration -> {
1✔
389
      // find the record and lock it until this transaction is complete
390
      SecretsRecord record = DSL.using(configuration)
1✔
391
          .select()
1✔
392
          .from(SECRETS)
1✔
393
          .where(SECRETS.ID.eq(id).and(SECRETS.CURRENT.isNotNull()))
1✔
394
          .forUpdate()
1✔
395
          .fetchOneInto(SECRETS);
1✔
396

397
      softDeleteSecretSeries(DSL.using(configuration), record);
1✔
398
    });
1✔
399
  }
1✔
400

401
  private static void softDeleteSecretSeries(DSLContext dslContext, SecretsRecord record) {
402
    if (record == null) {
1✔
403
      return;
×
404
    }
405

406
    long now = OffsetDateTime.now().toEpochSecond();
1✔
407

408
    dslContext
1✔
409
        .update(SECRETS)
1✔
410
        .set(SECRETS.NAME, transformNameForDeletion(record.getName()))
1✔
411
        .set(SECRETS.CURRENT, (Long) null)
1✔
412
        .set(SECRETS.UPDATEDAT, now)
1✔
413
        .where(SECRETS.ID.eq(record.getId()))
1✔
414
        .execute();
1✔
415

416
    dslContext
1✔
417
        .delete(ACCESSGRANTS)
1✔
418
        .where(ACCESSGRANTS.SECRETID.eq(record.getId()))
1✔
419
        .execute();
1✔
420
  }
1✔
421

422
  public void renameSecretSeriesById(long secretId, String name, String creator, long now) {
423
    String rowHmac = computeRowHmac(secretId, name);
1✔
424
    dslContext.update(SECRETS)
1✔
425
        .set(SECRETS.NAME, name)
1✔
426
        .set(SECRETS.ROW_HMAC, rowHmac)
1✔
427
        .set(SECRETS.UPDATEDBY, creator)
1✔
428
        .set(SECRETS.UPDATEDAT, now)
1✔
429
        .where(SECRETS.ID.eq(secretId))
1✔
430
        .execute();
1✔
431
  }
1✔
432

433
  /**
434
   * @return the number of deleted secret series
435
   */
436
  public int countDeletedSecretSeries() {
437
    return dslContext.selectCount()
1✔
438
        .from(SECRETS)
1✔
439
        .where(SECRETS.CURRENT.isNull())
1✔
440
        .fetchOne()
1✔
441
        .value1();
1✔
442
  }
443

444
  /**
445
   * Identify all secret series which were deleted before the given date.
446
   *
447
   * @param deleteBefore the cutoff date; secrets deleted before this date will be returned
448
   * @return IDs for secret series deleted before this date
449
   */
450
  public List<Long> getIdsForSecretSeriesDeletedBeforeDate(DateTime deleteBefore) {
451
    long deleteBeforeSeconds = deleteBefore.getMillis() / 1000;
1✔
452
    return dslContext.select(SECRETS.ID)
1✔
453
        .from(SECRETS)
1✔
454
        .where(SECRETS.CURRENT.isNull())
1✔
455
        .and(SECRETS.UPDATEDAT.le(deleteBeforeSeconds))
1✔
456
        .fetch(SECRETS.ID);
1✔
457
  }
458

459
  /**
460
   * PERMANENTLY REMOVE database records from `secrets` which have the given list of IDs. Does not
461
   * affect the `secrets_content` table.
462
   *
463
   * @param ids the IDs in the `secrets` table to be PERMANENTLY REMOVED
464
   * @return the number of records which were removed
465
   */
466
  public long dangerPermanentlyRemoveRecordsForGivenIDs(List<Long> ids) {
467
    return dslContext.deleteFrom(SECRETS)
1✔
468
        .where(SECRETS.ID.in(ids))
1✔
469
        .execute();
1✔
470
  }
471

472
  public static class SecretSeriesDAOFactory implements DAOFactory<SecretSeriesDAO> {
473
    private final DSLContext jooq;
474
    private final DSLContext readonlyJooq;
475
    private final ObjectMapper objectMapper;
476
    private final SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory;
477
    private final RowHmacGenerator rowHmacGenerator;
478

479
    @Inject public SecretSeriesDAOFactory(
480
        DSLContext jooq,
481
        @Readonly DSLContext readonlyJooq,
482
        ObjectMapper objectMapper,
483
        SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory,
484
        RowHmacGenerator rowHmacGenerator) {
1✔
485
      this.jooq = jooq;
1✔
486
      this.readonlyJooq = readonlyJooq;
1✔
487
      this.objectMapper = objectMapper;
1✔
488
      this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
489
      this.rowHmacGenerator = rowHmacGenerator;
1✔
490
    }
1✔
491

492
    @Override public SecretSeriesDAO readwrite() {
493
      return new SecretSeriesDAO(
1✔
494
          jooq,
495
          objectMapper,
496
          secretSeriesMapperFactory.using(jooq),
1✔
497
          rowHmacGenerator);
498
    }
499

500
    @Override public SecretSeriesDAO readonly() {
501
      return new SecretSeriesDAO(
×
502
          readonlyJooq,
503
          objectMapper,
504
          secretSeriesMapperFactory.using(readonlyJooq),
×
505
          rowHmacGenerator);
506
    }
507

508
    @Override public SecretSeriesDAO using(Configuration configuration) {
509
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
510
      return new SecretSeriesDAO(
1✔
511
          dslContext,
512
          objectMapper,
513
          secretSeriesMapperFactory.using(dslContext),
1✔
514
          rowHmacGenerator);
515
    }
516
  }
517

518
  // create a new name for the deleted secret, so that deleted secret names can be reused, while
519
  // still having a unique constraint on the name field in the DB
520
  private static String transformNameForDeletion(String name) {
521
    long now = OffsetDateTime.now().toEpochSecond();
1✔
522
    return String.format(".%s.deleted.%d.%s", name, now, UUID.randomUUID());
1✔
523
  }
524

525
  private String computeRowHmac(long secretSeriesId, String secretSeriesName) {
526
    return rowHmacGenerator.computeRowHmac(
1✔
527
        SECRETS.getName(),
1✔
528
        List.of(
1✔
529
            secretSeriesName,
530
            secretSeriesId));
1✔
531
  }
532
}
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