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

square / keywhiz / 3632963439

pending completion
3632963439

Pull #1163

github

GitHub
Merge 2c9d7585d into 5f84f8036
Pull Request #1163: Adding support for hard-deleting secrets

87 of 87 new or added lines in 6 files covered. (100.0%)

5177 of 6700 relevant lines covered (77.27%)

0.77 hits per line

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

97.64
/server/src/main/java/keywhiz/service/daos/AclDAO.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.google.common.collect.ImmutableMap;
20
import com.google.common.collect.ImmutableSet;
21
import java.time.Instant;
22
import java.time.OffsetDateTime;
23
import java.util.HashSet;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Optional;
27
import java.util.Set;
28
import java.util.stream.Collectors;
29
import javax.inject.Inject;
30
import keywhiz.KeywhizConfig;
31
import keywhiz.KeywhizConfig.RowHmacCheck;
32
import keywhiz.api.ApiDate;
33
import keywhiz.api.model.Client;
34
import keywhiz.api.model.Group;
35
import keywhiz.api.model.SanitizedSecret;
36
import keywhiz.api.model.Secret;
37
import keywhiz.api.model.SecretContent;
38
import keywhiz.api.model.SecretSeries;
39
import keywhiz.api.model.SecretSeriesAndContent;
40
import keywhiz.jooq.tables.records.SecretsRecord;
41
import keywhiz.log.AuditLog;
42
import keywhiz.log.Event;
43
import keywhiz.log.EventTag;
44
import keywhiz.service.config.Readonly;
45
import keywhiz.service.crypto.RowHmacGenerator;
46
import keywhiz.service.daos.ClientDAO.ClientDAOFactory;
47
import keywhiz.service.daos.GroupDAO.GroupDAOFactory;
48
import keywhiz.service.daos.SecretContentDAO.SecretContentDAOFactory;
49
import keywhiz.service.daos.SecretSeriesDAO.SecretSeriesDAOFactory;
50
import keywhiz.service.daos.SecretSeriesMapper.SecretSeriesMapperFactory;
51
import org.jooq.Configuration;
52
import org.jooq.DSLContext;
53
import org.jooq.Record;
54
import org.jooq.SelectQuery;
55
import org.jooq.impl.DSL;
56
import org.slf4j.Logger;
57
import org.slf4j.LoggerFactory;
58

59
import static com.google.common.base.Preconditions.checkArgument;
60
import static com.google.common.base.Preconditions.checkNotNull;
61
import static java.lang.String.format;
62
import static java.util.stream.Collectors.toList;
63
import static keywhiz.jooq.tables.Accessgrants.ACCESSGRANTS;
64
import static keywhiz.jooq.tables.Clients.CLIENTS;
65
import static keywhiz.jooq.tables.Groups.GROUPS;
66
import static keywhiz.jooq.tables.Memberships.MEMBERSHIPS;
67
import static keywhiz.jooq.tables.Secrets.SECRETS;
68
import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT;
69

70
public class AclDAO {
71
  private static final Logger logger = LoggerFactory.getLogger(AclDAO.class);
1✔
72

73
  private final DSLContext dslContext;
74
  private final ClientDAOFactory clientDAOFactory;
75
  private final GroupDAOFactory groupDAOFactory;
76
  private final SecretContentDAOFactory secretContentDAOFactory;
77
  private final SecretSeriesDAOFactory secretSeriesDAOFactory;
78
  private final ClientMapper clientMapper;
79
  private final GroupMapper groupMapper;
80
  private final SecretSeriesMapperFactory secretSeriesMapperFactory;
81
  private final SecretContentMapper secretContentMapper;
82
  private final RowHmacGenerator rowHmacGenerator;
83
  private final KeywhizConfig config;
84

85
  private AclDAO(DSLContext dslContext, ClientDAOFactory clientDAOFactory, GroupDAOFactory groupDAOFactory,
86
                 SecretContentDAOFactory secretContentDAOFactory, SecretSeriesDAOFactory secretSeriesDAOFactory,
87
                 ClientMapper clientMapper, GroupMapper groupMapper, SecretSeriesMapperFactory secretSeriesMapperFactory,
88
                 SecretContentMapper secretContentMapper, RowHmacGenerator rowHmacGenerator,
89
                 KeywhizConfig config) {
1✔
90
    this.dslContext = dslContext;
1✔
91
    this.clientDAOFactory = clientDAOFactory;
1✔
92
    this.groupDAOFactory = groupDAOFactory;
1✔
93
    this.secretContentDAOFactory = secretContentDAOFactory;
1✔
94
    this.secretSeriesDAOFactory = secretSeriesDAOFactory;
1✔
95
    this.clientMapper = clientMapper;
1✔
96
    this.groupMapper = groupMapper;
1✔
97
    this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
98
    this.secretContentMapper = secretContentMapper;
1✔
99
    this.rowHmacGenerator = rowHmacGenerator;
1✔
100
    this.config = config;
1✔
101
  }
1✔
102

103
  public void findAndAllowAccess(long secretId, long groupId, AuditLog auditLog, String user, Map<String, String> extraInfo) {
104
    dslContext.transaction(configuration -> {
1✔
105
      GroupDAO groupDAO = groupDAOFactory.using(configuration);
1✔
106
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
107

108
      Optional<Group> group = groupDAO.getGroupById(groupId);
1✔
109
      if (!group.isPresent()) {
1✔
110
        logger.info("Failure to allow access groupId {}, secretId {}: groupId not found.", groupId,
1✔
111
            secretId);
1✔
112
        throw new IllegalStateException(format("GroupId %d doesn't exist.", groupId));
1✔
113
      }
114

115
      Optional<SecretSeries> secret = secretSeriesDAO.getSecretSeriesById(secretId);
1✔
116
      if (!secret.isPresent()) {
1✔
117
        logger.info("Failure to allow access groupId {}, secretId {}: secretId not found.", groupId,
1✔
118
            secretId);
1✔
119
        throw new IllegalStateException(format("SecretId %d doesn't exist.", secretId));
1✔
120
      }
121

122
      allowAccess(configuration, secretId, groupId);
1✔
123

124
      extraInfo.put("group", group.get().getName());
1✔
125
      extraInfo.put("secret added", secret.get().name());
1✔
126
      auditLog.recordEvent(new Event(Instant.now(), EventTag.CHANGEACL_GROUP_SECRET, user, group.get().getName(), extraInfo));
1✔
127
    });
1✔
128
  }
1✔
129

130
  public void findAndRevokeAccess(long secretId, long groupId, AuditLog auditLog, String user, Map<String, String> extraInfo) {
131
    dslContext.transaction(configuration -> {
1✔
132
      GroupDAO groupDAO = groupDAOFactory.using(configuration);
1✔
133
      SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(configuration);
1✔
134

135
      Optional<Group> group = groupDAO.getGroupById(groupId);
1✔
136
      if (!group.isPresent()) {
1✔
137
        logger.info("Failure to revoke access groupId {}, secretId {}: groupId not found.", groupId,
×
138
            secretId);
×
139
        throw new IllegalStateException(format("GroupId %d doesn't exist.", groupId));
×
140
      }
141

142
      Optional<SecretSeries> secret = secretSeriesDAO.getSecretSeriesById(secretId);
1✔
143
      if (!secret.isPresent()) {
1✔
144
        logger.info("Failure to revoke access groupId {}, secretId {}: secretId not found.",
1✔
145
            groupId, secretId);
1✔
146
        throw new IllegalStateException(format("SecretId %d doesn't exist.", secretId));
1✔
147
      }
148

149
      revokeAccess(configuration, secretId, groupId);
1✔
150

151
      extraInfo.put("group", group.get().getName());
1✔
152
      extraInfo.put("secret removed", secret.get().name());
1✔
153
      auditLog.recordEvent(new Event(Instant.now(), EventTag.CHANGEACL_GROUP_SECRET, user, group.get().getName(), extraInfo));
1✔
154
    });
1✔
155
  }
1✔
156

157
  public void findAndEnrollClient(long clientId, long groupId, AuditLog auditLog, String user, Map<String, String> extraInfo) {
158
    dslContext.transaction(configuration -> {
1✔
159
      ClientDAO clientDAO = clientDAOFactory.using(configuration);
1✔
160
      GroupDAO groupDAO = groupDAOFactory.using(configuration);
1✔
161

162
      Optional<Client> client = clientDAO.getClientById(clientId);
1✔
163
      if (!client.isPresent()) {
1✔
164
        logger.info("Failure to enroll membership clientId {}, groupId {}: clientId not found.",
1✔
165
            clientId, groupId);
1✔
166
        throw new IllegalStateException(format("ClientId %d doesn't exist.", clientId));
1✔
167
      }
168

169
      Optional<Group> group = groupDAO.getGroupById(groupId);
1✔
170
      if (!group.isPresent()) {
1✔
171
        logger.info("Failure to enroll membership clientId {}, groupId {}: groupId not found.",
1✔
172
            clientId, groupId);
1✔
173
        throw new IllegalStateException(format("GroupId %d doesn't exist.", groupId));
1✔
174
      }
175

176
      enrollClient(configuration, clientId, groupId);
1✔
177

178
      extraInfo.put("group", group.get().getName());
1✔
179
      extraInfo.put("client added", client.get().getName());
1✔
180
      auditLog.recordEvent(new Event(Instant.now(), EventTag.CHANGEACL_GROUP_CLIENT, user, group.get().getName(), extraInfo));
1✔
181
    });
1✔
182
  }
1✔
183

184
  public void findAndEvictClient(long clientId, long groupId, AuditLog auditLog, String user, Map<String, String> extraInfo) {
185
    dslContext.transaction(configuration -> {
1✔
186
      ClientDAO clientDAO = clientDAOFactory.using(configuration);
1✔
187
      GroupDAO groupDAO = groupDAOFactory.using(configuration);
1✔
188

189
      Optional<Client> client = clientDAO.getClientById(clientId);
1✔
190
      if (!client.isPresent()) {
1✔
191
        logger.info("Failure to evict membership clientId {}, groupId {}: clientId not found.",
1✔
192
            clientId, groupId);
1✔
193
        throw new IllegalStateException(format("ClientId %d doesn't exist.", clientId));
1✔
194
      }
195

196
      Optional<Group> group = groupDAO.getGroupById(groupId);
1✔
197
      if (!group.isPresent()) {
1✔
198
        logger.info("Failure to evict membership clientId {}, groupId {}: groupId not found.",
1✔
199
            clientId, groupId);
1✔
200
        throw new IllegalStateException(format("GroupId %d doesn't exist.", groupId));
1✔
201
      }
202

203
      evictClient(configuration, clientId, groupId);
1✔
204

205
      extraInfo.put("group", group.get().getName());
1✔
206
      extraInfo.put("client removed", client.get().getName());
1✔
207
      auditLog.recordEvent(new Event(Instant.now(), EventTag.CHANGEACL_GROUP_CLIENT, user, group.get().getName(), extraInfo));
1✔
208
    });
1✔
209
  }
1✔
210

211
  public ImmutableSet<SanitizedSecret> getSanitizedSecretsFor(Group group) {
212
    return getSanitizedSecretsFor(group, false);
1✔
213
  }
214

215
  public ImmutableSet<SanitizedSecret> getSanitizedSecretsFor(Group group, boolean owned) {
216
    checkNotNull(group);
1✔
217

218
    ImmutableSet.Builder<SanitizedSecret> set = ImmutableSet.builder();
1✔
219

220
    return dslContext.transactionResult(configuration -> {
1✔
221
      SecretContentDAO secretContentDAO = secretContentDAOFactory.using(configuration);
1✔
222

223
      Set<SecretSeries> seriesSet;
224
      if (owned) {
1✔
225
        seriesSet = getOwnedSecretSeriesFor(configuration, group);
1✔
226
      } else {
227
        seriesSet = getSecretSeriesFor(configuration, group);
1✔
228
      }
229

230
      for (SecretSeries series : seriesSet) {
1✔
231
        SecretContent content = secretContentDAO.getSecretContentById(series.currentVersion().get()).get();
1✔
232
        SecretSeriesAndContent seriesAndContent = SecretSeriesAndContent.of(series, content);
1✔
233
        set.add(SanitizedSecret.fromSecretSeriesAndContent(seriesAndContent));
1✔
234
      }
1✔
235

236
      return set.build();
1✔
237
    });
238
  }
239

240
  public Set<Group> getGroupsFor(Secret secret) {
241
    List<Group> r = dslContext
1✔
242
        .select(GROUPS.fields())
1✔
243
        .from(GROUPS)
1✔
244
        .join(ACCESSGRANTS).on(GROUPS.ID.eq(ACCESSGRANTS.GROUPID))
1✔
245
        .join(SECRETS).on(ACCESSGRANTS.SECRETID.eq(SECRETS.ID))
1✔
246
        .where(SECRETS.NAME.eq(secret.getName()))
1✔
247
        .fetchInto(GROUPS)
1✔
248
        .map(groupMapper);
1✔
249
    return new HashSet<>(r);
1✔
250
  }
251

252
  public Set<Group> getGroupsFor(Client client) {
253
    return getGroupsFor(dslContext, client);
1✔
254
  }
255

256
  public Set<Group> getGroupsFor(DSLContext dslContext, Client client) {
257
    List<Group> r = dslContext
1✔
258
        .select(GROUPS.fields())
1✔
259
        .from(GROUPS)
1✔
260
        .join(MEMBERSHIPS).on(GROUPS.ID.eq(MEMBERSHIPS.GROUPID))
1✔
261
        .join(CLIENTS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
262
        .where(CLIENTS.NAME.eq(client.getName()))
1✔
263
        .fetchInto(GROUPS)
1✔
264
        .map(groupMapper);
1✔
265
    return new HashSet<>(r);
1✔
266
  }
267

268
  public Set<Client> getClientsFor(Group group) {
269
    List<Client> r = dslContext
1✔
270
        .select(CLIENTS.fields())
1✔
271
        .from(CLIENTS)
1✔
272
        .join(MEMBERSHIPS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
273
        .join(GROUPS).on(GROUPS.ID.eq(MEMBERSHIPS.GROUPID))
1✔
274
        .where(GROUPS.NAME.eq(group.getName()))
1✔
275
        .fetchInto(CLIENTS)
1✔
276
        .map(clientMapper);
1✔
277
    return new HashSet<>(r);
1✔
278
  }
279

280
  public ImmutableSet<SanitizedSecret> getSanitizedSecretsFor(Client client) {
281
    checkNotNull(client);
1✔
282

283
    ImmutableSet.Builder<SanitizedSecret> sanitizedSet = ImmutableSet.builder();
1✔
284

285
    SelectQuery<Record> query = dslContext.select(SECRETS.fields())
1✔
286
        .from(SECRETS)
1✔
287
        .join(ACCESSGRANTS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
288
        .join(MEMBERSHIPS).on(ACCESSGRANTS.GROUPID.eq(MEMBERSHIPS.GROUPID))
1✔
289
        .join(CLIENTS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
290
        .join(SECRETS_CONTENT).on(SECRETS_CONTENT.ID.eq(SECRETS.CURRENT))
1✔
291
        .where(CLIENTS.NAME.eq(client.getName()).and(SECRETS.CURRENT.isNotNull()))
1✔
292
        .getQuery();
1✔
293
    query.addSelect(SECRETS_CONTENT.CONTENT_HMAC);
1✔
294
    query.addSelect(SECRETS_CONTENT.CREATEDAT);
1✔
295
    query.addSelect(SECRETS_CONTENT.CREATEDBY);
1✔
296
    query.addSelect(SECRETS_CONTENT.METADATA);
1✔
297
    query.addSelect(SECRETS_CONTENT.EXPIRY);
1✔
298
    query.addSelect(ACCESSGRANTS.ROW_HMAC);
1✔
299
    query.addSelect(MEMBERSHIPS.ROW_HMAC);
1✔
300
    query.addSelect(MEMBERSHIPS.GROUPID);
1✔
301
    query.addSelect(CLIENTS.ROW_HMAC);
1✔
302
    query.addSelect(SECRETS.ROW_HMAC);
1✔
303
    query.fetch()
1✔
304
        .map(row -> processSanitizedSecretRow(row, client))
1✔
305
        .forEach(sanitizedSet::add);
1✔
306

307
    return sanitizedSet.build();
1✔
308
  }
309

310
  public Set<Client> getClientsFor(Secret secret) {
311
    List<Client> r = dslContext
1✔
312
        .select(CLIENTS.fields())
1✔
313
        .from(CLIENTS)
1✔
314
        .join(MEMBERSHIPS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
315
        .join(ACCESSGRANTS).on(MEMBERSHIPS.GROUPID.eq(ACCESSGRANTS.GROUPID))
1✔
316
        .join(SECRETS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
317
        .where(SECRETS.NAME.eq(secret.getName()))
1✔
318
        .fetchInto(CLIENTS)
1✔
319
        .map(clientMapper);
1✔
320
    return new HashSet<>(r);
1✔
321
  }
322

323
  public Optional<SanitizedSecret> getSanitizedSecretFor(Client client, String secretName) {
324
    checkNotNull(client);
1✔
325
    checkArgument(!secretName.isEmpty());
1✔
326

327
    SelectQuery<Record> query = dslContext.select(SECRETS.fields())
1✔
328
        .from(SECRETS)
1✔
329
        .join(ACCESSGRANTS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
330
        .join(MEMBERSHIPS).on(ACCESSGRANTS.GROUPID.eq(MEMBERSHIPS.GROUPID))
1✔
331
        .join(CLIENTS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
332
        .join(SECRETS_CONTENT).on(SECRETS_CONTENT.ID.eq(SECRETS.CURRENT))
1✔
333
        .where(CLIENTS.NAME.eq(client.getName())
1✔
334
            .and(SECRETS.CURRENT.isNotNull())
1✔
335
            .and(SECRETS.NAME.eq(secretName)))
1✔
336
        .limit(1)
1✔
337
        .getQuery();
1✔
338
    query.addSelect(SECRETS_CONTENT.CONTENT_HMAC);
1✔
339
    query.addSelect(SECRETS_CONTENT.CREATEDAT);
1✔
340
    query.addSelect(SECRETS_CONTENT.CREATEDBY);
1✔
341
    query.addSelect(SECRETS_CONTENT.METADATA);
1✔
342
    query.addSelect(SECRETS_CONTENT.EXPIRY);
1✔
343
    query.addSelect(ACCESSGRANTS.ROW_HMAC);
1✔
344
    query.addSelect(MEMBERSHIPS.ROW_HMAC);
1✔
345
    query.addSelect(MEMBERSHIPS.GROUPID);
1✔
346
    query.addSelect(CLIENTS.ROW_HMAC);
1✔
347
    query.addSelect(SECRETS.ROW_HMAC);
1✔
348
    return Optional.ofNullable(query.fetchOne())
1✔
349
        .map(row -> processSanitizedSecretRow(row, client));
1✔
350
  }
351

352
  public List<SanitizedSecret> getBatchSanitizedSecretsFor(Client client, List<String> secretNames) {
353
    checkNotNull(client);
1✔
354
    checkArgument(!secretNames.isEmpty());
1✔
355

356
    SelectQuery<Record> query = dslContext.select(SECRETS.fields())
1✔
357
            .from(SECRETS)
1✔
358
            .join(ACCESSGRANTS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
359
            .join(MEMBERSHIPS).on(ACCESSGRANTS.GROUPID.eq(MEMBERSHIPS.GROUPID))
1✔
360
            .join(CLIENTS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
361
            .join(SECRETS_CONTENT).on(SECRETS_CONTENT.ID.eq(SECRETS.CURRENT))
1✔
362
            .where(CLIENTS.NAME.eq(client.getName())
1✔
363
                    .and(SECRETS.CURRENT.isNotNull())
1✔
364
                    .and(SECRETS.NAME.in(secretNames)))
1✔
365
            .getQuery();
1✔
366
    query.addSelect(SECRETS_CONTENT.CONTENT_HMAC);
1✔
367
    query.addSelect(SECRETS_CONTENT.CREATEDAT);
1✔
368
    query.addSelect(SECRETS_CONTENT.CREATEDBY);
1✔
369
    query.addSelect(SECRETS_CONTENT.METADATA);
1✔
370
    query.addSelect(SECRETS_CONTENT.EXPIRY);
1✔
371
    query.addSelect(ACCESSGRANTS.ROW_HMAC);
1✔
372
    query.addSelect(MEMBERSHIPS.ROW_HMAC);
1✔
373
    query.addSelect(MEMBERSHIPS.GROUPID);
1✔
374
    query.addSelect(CLIENTS.ROW_HMAC);
1✔
375
    query.addSelect(SECRETS.ROW_HMAC);
1✔
376

377
    return query.fetch().map(row -> processSanitizedSecretRow(row, client));
1✔
378
  }
379

380
  private SanitizedSecret processSanitizedSecretRow(Record row, Client client) {
381
    boolean rowHmacLog = config.getRowHmacCheck() == RowHmacCheck.DISABLED_BUT_LOG;
1✔
382
    boolean rowHmacFail = config.getRowHmacCheck() == RowHmacCheck.ENFORCED;
1✔
383

384
    SecretSeries series = secretSeriesMapperFactory.using(dslContext).map(row.into(SECRETS));
1✔
385

386
    String secretHmac = rowHmacGenerator.computeRowHmac(
1✔
387
        SECRETS.getName(), List.of(row.getValue(SECRETS.NAME), row.getValue(SECRETS.ID))
1✔
388
    );
389
    if (!RowHmacGenerator.compareHmacs(secretHmac, row.getValue(SECRETS.ROW_HMAC))) {
1✔
390
      String errorMessage = String.format(
1✔
391
          "Secret HMAC verification failed for secret: %s", row.getValue(SECRETS.NAME));
1✔
392
      if (rowHmacLog) {
1✔
393
        logger.warn(errorMessage);
×
394
      }
395
      if (rowHmacFail) {
1✔
396
        throw new AssertionError(errorMessage);
1✔
397
      }
398
    }
399

400
    String clientHmac = rowHmacGenerator.computeRowHmac(
1✔
401
        CLIENTS.getName(), List.of(client.getName(), client.getId())
1✔
402
    );
403
    if (!RowHmacGenerator.compareHmacs(clientHmac, row.getValue(CLIENTS.ROW_HMAC))) {
1✔
404
      String errorMessage = String.format(
1✔
405
          "Client HMAC verification failed for client: %s", client.getName());
1✔
406
      if (rowHmacLog) {
1✔
407
        logger.warn(errorMessage);
×
408
      }
409
      if (rowHmacFail) {
1✔
410
        throw new AssertionError(errorMessage);
1✔
411
      }
412
    }
413

414
    String membershipsHmac = rowHmacGenerator.computeRowHmac(
1✔
415
        MEMBERSHIPS.getName(), List.of(client.getId(), row.getValue(MEMBERSHIPS.GROUPID)));
1✔
416
    if (!RowHmacGenerator.compareHmacs(membershipsHmac, row.getValue(MEMBERSHIPS.ROW_HMAC))) {
1✔
417
      String errorMessage = String.format(
1✔
418
          "Memberships HMAC verification failed for clientId: %d in groupId: %d",
419
          client.getId(), row.getValue(MEMBERSHIPS.GROUPID));
1✔
420
      if (rowHmacLog) {
1✔
421
        logger.warn(errorMessage);
×
422
      }
423
      if (rowHmacFail) {
1✔
424
        throw new AssertionError(errorMessage);
1✔
425
      }
426
    }
427

428
    String accessgrantsHmac = rowHmacGenerator.computeRowHmac(
1✔
429
        ACCESSGRANTS.getName(),
1✔
430
        List.of(row.getValue(MEMBERSHIPS.GROUPID), row.getValue(SECRETS.ID)));
1✔
431
    if (!RowHmacGenerator.compareHmacs(accessgrantsHmac, row.getValue(ACCESSGRANTS.ROW_HMAC))) {
1✔
432
      String errorMessage = String.format(
1✔
433
          "Access Grants HMAC verification failed for groupId: %d in secretId: %d",
434
          row.getValue(MEMBERSHIPS.GROUPID), row.getValue(SECRETS.ID));
1✔
435
      if (rowHmacLog) {
1✔
436
        logger.warn(errorMessage);
×
437
      }
438
      if (rowHmacFail) {
1✔
439
        throw new AssertionError(errorMessage);
1✔
440
      }
441
    }
442

443
    return SanitizedSecret.of(
1✔
444
        series.id(),
1✔
445
        series.name(),
1✔
446
        series.owner(),
1✔
447
        series.description(),
1✔
448
        row.getValue(SECRETS_CONTENT.CONTENT_HMAC),
1✔
449
        series.createdAt(),
1✔
450
        series.createdBy(),
1✔
451
        series.updatedAt(),
1✔
452
        series.updatedBy(),
1✔
453
        secretContentMapper.tryToReadMapFromMetadata(row.getValue(SECRETS_CONTENT.METADATA)),
1✔
454
        series.type().orElse(null),
1✔
455
        series.generationOptions(),
1✔
456
        row.getValue(SECRETS_CONTENT.EXPIRY),
1✔
457
        series.currentVersion().orElse(null),
1✔
458
        new ApiDate(row.getValue(SECRETS_CONTENT.CREATEDAT)),
1✔
459
        row.getValue(SECRETS_CONTENT.CREATEDBY));
1✔
460
  }
461

462
  public Map<Long, List<Group>> getGroupsForSecrets(Set<Long> secretIdList) {
463
    Map<Long, Group> groupMap = dslContext.select().from(GROUPS)
1✔
464
        .join(ACCESSGRANTS).on(ACCESSGRANTS.GROUPID.eq(GROUPS.ID))
1✔
465
        .join(SECRETS).on(ACCESSGRANTS.SECRETID.eq(SECRETS.ID))
1✔
466
        .where(SECRETS.ID.in(secretIdList))
1✔
467
        .fetchInto(GROUPS).map(groupMapper).stream().collect(Collectors.toMap(Group::getId, g -> g, (g1, g2) -> g1));
1✔
468

469
    Map<Long, List<Long>> secretsIdGroupsIdMap = dslContext.select().from(GROUPS)
1✔
470
        .join(ACCESSGRANTS).on(ACCESSGRANTS.GROUPID.eq(GROUPS.ID))
1✔
471
        .join(SECRETS).on(ACCESSGRANTS.SECRETID.eq(SECRETS.ID))
1✔
472
        .where(SECRETS.ID.in(secretIdList))
1✔
473
        .fetch().intoGroups(SECRETS.ID, GROUPS.ID);
1✔
474

475
    ImmutableMap.Builder<Long, List<Group>> builder = ImmutableMap.builder();
1✔
476
    for (Map.Entry<Long, List<Long>> entry : secretsIdGroupsIdMap.entrySet()) {
1✔
477
      List<Group> groupList = entry.getValue().stream().map(groupMap::get).collect(toList());
1✔
478
      builder.put(entry.getKey(), groupList);
1✔
479
    }
1✔
480
    return builder.build();
1✔
481
  }
482

483
  protected void allowAccess(Configuration configuration, long secretId, long groupId) {
484
    long now = OffsetDateTime.now().toEpochSecond();
1✔
485

486
    boolean assigned = 0 < DSL.using(configuration)
1✔
487
        .fetchCount(ACCESSGRANTS,
1✔
488
            ACCESSGRANTS.SECRETID.eq(secretId).and(
1✔
489
            ACCESSGRANTS.GROUPID.eq(groupId)));
1✔
490
    if (assigned) {
1✔
491
      return;
1✔
492
    }
493

494
    String verificationHmac = rowHmacGenerator.computeRowHmac(
1✔
495
        ACCESSGRANTS.getName(), List.of(groupId, secretId));
1✔
496

497
    DSL.using(configuration)
1✔
498
        .insertInto(ACCESSGRANTS)
1✔
499
        .set(ACCESSGRANTS.SECRETID, secretId)
1✔
500
        .set(ACCESSGRANTS.GROUPID, groupId)
1✔
501
        .set(ACCESSGRANTS.CREATEDAT, now)
1✔
502
        .set(ACCESSGRANTS.UPDATEDAT, now)
1✔
503
        .set(ACCESSGRANTS.ROW_HMAC, verificationHmac)
1✔
504
        .execute();
1✔
505
  }
1✔
506

507
  protected void revokeAccess(Configuration configuration, long secretId, long groupId) {
508
    DSL.using(configuration)
1✔
509
        .delete(ACCESSGRANTS)
1✔
510
        .where(ACCESSGRANTS.SECRETID.eq(secretId)
1✔
511
            .and(ACCESSGRANTS.GROUPID.eq(groupId)))
1✔
512
        .execute();
1✔
513
  }
1✔
514

515
  protected void enrollClient(Configuration configuration, long clientId, long groupId) {
516
    long now = OffsetDateTime.now().toEpochSecond();
1✔
517

518
    boolean enrolled = 0 < DSL.using(configuration)
1✔
519
        .fetchCount(MEMBERSHIPS,
1✔
520
            MEMBERSHIPS.GROUPID.eq(groupId).and(
1✔
521
            MEMBERSHIPS.CLIENTID.eq(clientId)));
1✔
522
    if (enrolled) {
1✔
523
      return;
1✔
524
    }
525

526
    String verificationHmac = rowHmacGenerator.computeRowHmac(
1✔
527
        MEMBERSHIPS.getName(), List.of(clientId, groupId));
1✔
528

529
    DSL.using(configuration)
1✔
530
        .insertInto(MEMBERSHIPS)
1✔
531
        .set(MEMBERSHIPS.GROUPID, groupId)
1✔
532
        .set(MEMBERSHIPS.CLIENTID, clientId)
1✔
533
        .set(MEMBERSHIPS.CREATEDAT, now)
1✔
534
        .set(MEMBERSHIPS.UPDATEDAT, now)
1✔
535
        .set(MEMBERSHIPS.ROW_HMAC, verificationHmac)
1✔
536
        .execute();
1✔
537
  }
1✔
538

539

540
  protected void evictClient(Configuration configuration, long clientId, long groupId) {
541
    DSL.using(configuration)
1✔
542
        .delete(MEMBERSHIPS)
1✔
543
        .where(MEMBERSHIPS.CLIENTID.eq(clientId)
1✔
544
            .and(MEMBERSHIPS.GROUPID.eq(groupId)))
1✔
545
        .execute();
1✔
546
  }
1✔
547

548
  public ImmutableSet<SecretSeries> getSecretSeriesFor(Group group) {
549
    checkNotNull(group);
1✔
550

551
    return dslContext.transactionResult(configuration -> getSecretSeriesFor(configuration, group));
1✔
552
  }
553

554
  protected ImmutableSet<SecretSeries> getSecretSeriesFor(Configuration configuration, Group group) {
555
    List<SecretSeries> r = DSL.using(configuration)
1✔
556
        .select(SECRETS.fields())
1✔
557
        .from(SECRETS)
1✔
558
        .join(ACCESSGRANTS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
559
        .join(GROUPS).on(GROUPS.ID.eq(ACCESSGRANTS.GROUPID))
1✔
560
        .where(GROUPS.NAME.eq(group.getName()).and(SECRETS.CURRENT.isNotNull()))
1✔
561
        .fetchInto(SECRETS)
1✔
562
        .map(secretSeriesMapperFactory.using(configuration.dsl()));
1✔
563
    return ImmutableSet.copyOf(r);
1✔
564
  }
565

566
  protected ImmutableSet<SecretSeries> getOwnedSecretSeriesFor(Configuration configuration, Group group) {
567
    List<SecretSeries> r = DSL.using(configuration)
1✔
568
        .select(SECRETS.fields())
1✔
569
        .from(SECRETS)
1✔
570
        .where(SECRETS.OWNER.eq(group.getId()))
1✔
571
        .and(SECRETS.CURRENT.isNotNull())
1✔
572
        .fetchInto(SECRETS)
1✔
573
        .map(secretSeriesMapperFactory.using(configuration.dsl()));
1✔
574
    return ImmutableSet.copyOf(r);
1✔
575
  }
576

577
  /**
578
   * @param configuration database information
579
   * @param client client to access secrets
580
   * @param secretName name of SecretSeries
581
   * @return Optional.absent() when secret unauthorized or not found.
582
   * The query doesn't distinguish between these cases. If result absent, a followup call on clients
583
   * table should be used to determine the exception.
584
   */
585
  protected Optional<SecretSeries> getSecretSeriesFor(Configuration configuration, Client client, String secretName) {
586
    // TODO: We need to set limit(1) because we are using joins. We should probably change the join type.
587
    SecretsRecord r = DSL.using(configuration)
1✔
588
        .select(SECRETS.fields())
1✔
589
        .from(SECRETS)
1✔
590
        .join(ACCESSGRANTS).on(SECRETS.ID.eq(ACCESSGRANTS.SECRETID))
1✔
591
        .join(MEMBERSHIPS).on(ACCESSGRANTS.GROUPID.eq(MEMBERSHIPS.GROUPID))
1✔
592
        .join(CLIENTS).on(CLIENTS.ID.eq(MEMBERSHIPS.CLIENTID))
1✔
593
        .where(SECRETS.NAME.eq(secretName).and(CLIENTS.NAME.eq(client.getName())).and(SECRETS.CURRENT.isNotNull()))
1✔
594
        .limit(1)
1✔
595
        .fetchOneInto(SECRETS);
1✔
596
    return Optional.ofNullable(r).map(secretSeriesMapperFactory.using(configuration.dsl())::map);
1✔
597
  }
598

599
  public static class AclDAOFactory implements DAOFactory<AclDAO> {
600
    private final DSLContext jooq;
601
    private final DSLContext readonlyJooq;
602
    private final ClientDAOFactory clientDAOFactory;
603
    private final GroupDAOFactory groupDAOFactory;
604
    private final SecretContentDAOFactory secretContentDAOFactory;
605
    private final SecretSeriesDAOFactory secretSeriesDAOFactory;
606
    private final ClientMapper clientMapper;
607
    private final GroupMapper groupMapper;
608
    private final SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory;
609
    private final SecretContentMapper secretContentMapper;
610
    private final RowHmacGenerator rowHmacGenerator;
611
    private final KeywhizConfig config;
612

613
    @Inject public AclDAOFactory(
614
        DSLContext jooq,
615
        @Readonly DSLContext readonlyJooq,
616
        ClientDAOFactory clientDAOFactory,
617
        GroupDAOFactory groupDAOFactory,
618
        SecretContentDAOFactory secretContentDAOFactory,
619
        SecretSeriesDAOFactory secretSeriesDAOFactory,
620
        ClientMapper clientMapper,
621
        GroupMapper groupMapper,
622
        SecretSeriesMapper.SecretSeriesMapperFactory secretSeriesMapperFactory,
623
        SecretContentMapper secretContentMapper,
624
        RowHmacGenerator rowHmacGenerator,
625
        KeywhizConfig config) {
1✔
626
      this.jooq = jooq;
1✔
627
      this.readonlyJooq = readonlyJooq;
1✔
628
      this.clientDAOFactory = clientDAOFactory;
1✔
629
      this.groupDAOFactory = groupDAOFactory;
1✔
630
      this.secretContentDAOFactory = secretContentDAOFactory;
1✔
631
      this.secretSeriesDAOFactory = secretSeriesDAOFactory;
1✔
632
      this.clientMapper = clientMapper;
1✔
633
      this.groupMapper = groupMapper;
1✔
634
      this.secretSeriesMapperFactory = secretSeriesMapperFactory;
1✔
635
      this.secretContentMapper = secretContentMapper;
1✔
636
      this.rowHmacGenerator = rowHmacGenerator;
1✔
637
      this.config = config;
1✔
638
    }
1✔
639

640
    @Override public AclDAO readwrite() {
641
      return new AclDAO(
1✔
642
          jooq,
643
          clientDAOFactory,
644
          groupDAOFactory,
645
          secretContentDAOFactory,
646
          secretSeriesDAOFactory,
647
          clientMapper,
648
          groupMapper,
649
          secretSeriesMapperFactory,
650
          secretContentMapper,
651
          rowHmacGenerator,
652
          config);
653
    }
654

655
    @Override public AclDAO readonly() {
656
      return new AclDAO(
1✔
657
          readonlyJooq,
658
          clientDAOFactory,
659
          groupDAOFactory,
660
          secretContentDAOFactory,
661
          secretSeriesDAOFactory,
662
          clientMapper,
663
          groupMapper,
664
          secretSeriesMapperFactory,
665
          secretContentMapper,
666
          rowHmacGenerator,
667
          config);
668
    }
669

670
    @Override public AclDAO using(Configuration configuration) {
671
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
×
672
      return new AclDAO(
×
673
          dslContext,
674
          clientDAOFactory,
675
          groupDAOFactory,
676
          secretContentDAOFactory,
677
          secretSeriesDAOFactory,
678
          clientMapper,
679
          groupMapper,
680
          secretSeriesMapperFactory,
681
          secretContentMapper,
682
          rowHmacGenerator,
683
          config);
684
    }
685
  }
686
}
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