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

square / keywhiz / 3626523165

pending completion
3626523165

push

github

GitHub
Adding owner support to ClientDAO (#1167)

55 of 55 new or added lines in 3 files covered. (100.0%)

5023 of 6502 relevant lines covered (77.25%)

0.77 hits per line

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

95.41
/server/src/main/java/keywhiz/service/daos/ClientDAO.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.ImmutableSet;
20
import java.net.URI;
21
import java.security.Principal;
22
import java.time.Duration;
23
import java.time.Instant;
24
import java.time.OffsetDateTime;
25
import java.util.List;
26
import java.util.Optional;
27
import java.util.Set;
28
import java.util.stream.Collectors;
29
import javax.annotation.Nullable;
30
import javax.inject.Inject;
31
import keywhiz.api.model.Client;
32
import keywhiz.auth.mutualssl.CertificatePrincipal;
33
import keywhiz.jooq.tables.Groups;
34
import keywhiz.jooq.tables.records.ClientsRecord;
35
import keywhiz.jooq.tables.records.GroupsRecord;
36
import keywhiz.service.config.Readonly;
37
import keywhiz.service.crypto.RowHmacGenerator;
38
import org.jooq.Condition;
39
import org.jooq.Configuration;
40
import org.jooq.DSLContext;
41
import org.jooq.Param;
42
import org.jooq.Record;
43
import org.jooq.Result;
44
import org.jooq.impl.DSL;
45

46
import static com.google.common.base.Preconditions.checkNotNull;
47
import static java.time.Instant.EPOCH;
48
import static keywhiz.jooq.tables.Clients.CLIENTS;
49
import static keywhiz.jooq.tables.Groups.GROUPS;
50
import static keywhiz.jooq.tables.Memberships.MEMBERSHIPS;
51
import static org.jooq.impl.DSL.greatest;
52
import static org.jooq.impl.DSL.when;
53

54
public class ClientDAO {
55
  private static final Groups CLIENT_OWNERS = GROUPS.as("owners");
1✔
56
  private static final Duration LAST_SEEN_THRESHOLD = Duration.ofSeconds(24 * 60 * 60);
1✔
57
  private static final Long NO_OWNER = null;
1✔
58

59
  private final DSLContext dslContext;
60
  private final ClientMapper clientMapper;
61
  private final RowHmacGenerator rowHmacGenerator;
62

63
  private ClientDAO(DSLContext dslContext, ClientMapper clientMapper,
64
      RowHmacGenerator rowHmacGenerator) {
1✔
65
    this.dslContext = dslContext;
1✔
66
    this.clientMapper = clientMapper;
1✔
67
    this.rowHmacGenerator = rowHmacGenerator;
1✔
68
  }
1✔
69

70
  public long createClient(
71
      String name,
72
      String user,
73
      String description,
74
      @Nullable URI spiffeId) {
75
    return createClient(
1✔
76
        name,
77
        user,
78
        description,
79
        spiffeId,
80
        NO_OWNER);
81
  }
82

83
  public long createClient(
84
      String name,
85
      String user,
86
      String description,
87
      @Nullable URI spiffeId,
88
      @Nullable Long ownerId) {
89
    ClientsRecord r = dslContext.newRecord(CLIENTS);
1✔
90

91
    long now = OffsetDateTime.now().toEpochSecond();
1✔
92

93
    long generatedId = rowHmacGenerator.getNextLongSecure();
1✔
94
    String rowHmac = rowHmacGenerator.computeRowHmac(
1✔
95
        CLIENTS.getName(), List.of(name, generatedId));
1✔
96

97
    // Do not allow empty spiffe URIs
98
    String spiffeStr = null;
1✔
99
    if (spiffeId != null && !spiffeId.toASCIIString().isEmpty()) {
1✔
100
      spiffeStr = spiffeId.toASCIIString();
1✔
101
    }
102

103
    r.setId(generatedId);
1✔
104
    r.setName(name);
1✔
105
    r.setCreatedby(user);
1✔
106
    r.setCreatedat(now);
1✔
107
    r.setUpdatedby(user);
1✔
108
    r.setUpdatedat(now);
1✔
109
    r.setLastseen(null);
1✔
110
    r.setDescription(description);
1✔
111
    r.setEnabled(true);
1✔
112
    r.setAutomationallowed(false);
1✔
113
    r.setSpiffeId(spiffeStr);
1✔
114
    r.setRowHmac(rowHmac);
1✔
115
    r.setOwner(ownerId);
1✔
116
    r.store();
1✔
117

118
    return r.getId();
1✔
119
  }
120

121
  public void deleteClient(Client client) {
122
    dslContext.transaction(configuration -> {
1✔
123
      DSL.using(configuration)
1✔
124
          .delete(CLIENTS)
1✔
125
          .where(CLIENTS.ID.eq(client.getId()))
1✔
126
          .execute();
1✔
127

128
      DSL.using(configuration)
1✔
129
          .delete(MEMBERSHIPS)
1✔
130
          .where(MEMBERSHIPS.CLIENTID.eq(client.getId()))
1✔
131
          .execute();
1✔
132
    });
1✔
133
  }
1✔
134

135
  public void sawClient(Client client, @Nullable Principal principal) {
136
    Instant now = Instant.now();
1✔
137

138
    Instant lastSeen = Optional.ofNullable(client.getLastSeen())
1✔
139
        .map(ls -> Instant.ofEpochSecond(ls.toEpochSecond()))
1✔
140
        .orElse(EPOCH);
1✔
141

142
    final Instant expiration;
143
    if (principal instanceof CertificatePrincipal) {
1✔
144
      expiration = ((CertificatePrincipal) principal).getCertificateExpiration();
1✔
145
    } else {
146
      expiration = EPOCH;
×
147
    }
148

149
    // Only update last seen if it's been more than `lastSeenThreshold` seconds
150
    // this way we can have less granularity on lastSeen and save DB writes
151
    if (now.isAfter(lastSeen.plus(LAST_SEEN_THRESHOLD))) {
1✔
152
      dslContext.transaction(configuration -> {
1✔
153
        Param<Long> lastSeenValue = DSL.val(now.getEpochSecond(), CLIENTS.LASTSEEN);
1✔
154
        Param<Long> expirationValue = DSL.val(expiration.getEpochSecond(), CLIENTS.EXPIRATION);
1✔
155

156
        DSL.using(configuration)
1✔
157
            .update(CLIENTS)
1✔
158
            .set(CLIENTS.LASTSEEN,
1✔
159
                when(CLIENTS.LASTSEEN.isNull(), lastSeenValue)
1✔
160
                    .otherwise(greatest(CLIENTS.LASTSEEN, lastSeenValue)))
1✔
161
            .set(CLIENTS.EXPIRATION, expirationValue)
1✔
162
            .where(CLIENTS.ID.eq(client.getId()))
1✔
163
            .execute();
1✔
164
      });
1✔
165
    }
166
  }
1✔
167

168
  public Optional<Client> getClientByName(String name) {
169
    return getClient(CLIENTS.NAME.eq(name));
1✔
170
  }
171

172
  public Optional<Client> getClientBySpiffeId(URI spiffeId) {
173
    return getClient(CLIENTS.SPIFFE_ID.eq(spiffeId.toASCIIString()));
1✔
174
  }
175

176
  public Optional<Client> getClientById(long id) {
177
    return getClient(CLIENTS.ID.eq(id));
1✔
178
  }
179

180
  private Optional<Client> getClient(Condition condition) {
181
    Record record = dslContext
1✔
182
        .select(CLIENTS.fields())
1✔
183
        .select(CLIENT_OWNERS.ID, CLIENT_OWNERS.NAME)
1✔
184
        .from(CLIENTS)
1✔
185
        .leftJoin(CLIENT_OWNERS)
1✔
186
        .on(CLIENTS.OWNER.eq(CLIENT_OWNERS.ID))
1✔
187
        .where(condition)
1✔
188
        .fetchOne();
1✔
189

190
    return Optional.ofNullable(recordToClient(record));
1✔
191
  }
192

193
  private Client recordToClient(Record record) {
194
    if (record == null) {
1✔
195
      return null;
1✔
196
    }
197

198
    ClientsRecord clientRecord = record.into(CLIENTS);
1✔
199
    GroupsRecord ownerRecord = record.into(CLIENT_OWNERS);
1✔
200

201
    boolean danglingOwner = clientRecord.getOwner() != null && ownerRecord.getId() == null;
1✔
202
    if (danglingOwner) {
1✔
203
      throw new IllegalStateException(
×
204
          String.format(
×
205
              "Owner %s for client %s is missing.",
206
              clientRecord.getOwner(),
×
207
              clientRecord.getName()));
×
208
    }
209

210
    Client client = clientMapper.map(clientRecord);
1✔
211
    if (ownerRecord != null) {
1✔
212
      client.setOwner(ownerRecord.getName());
1✔
213
    }
214

215
    return client;
1✔
216
  }
217

218
  public ImmutableSet<Client> getClients() {
219
    List<Client> clients = dslContext
1✔
220
        .select(CLIENTS.fields())
1✔
221
        .select(CLIENT_OWNERS.NAME)
1✔
222
        .from(CLIENTS)
1✔
223
        .leftJoin(CLIENT_OWNERS)
1✔
224
        .on(CLIENTS.OWNER.eq(CLIENT_OWNERS.ID))
1✔
225
        .fetch()
1✔
226
        .map(this::recordToClient);
1✔
227

228
    return ImmutableSet.copyOf(clients);
1✔
229
  }
230

231
  public static class ClientDAOFactory implements DAOFactory<ClientDAO> {
232
    private final DSLContext jooq;
233
    private final DSLContext readonlyJooq;
234
    private final ClientMapper clientMapper;
235
    private final RowHmacGenerator rowHmacGenerator;
236

237
    @Inject public ClientDAOFactory(DSLContext jooq, @Readonly DSLContext readonlyJooq,
238
        ClientMapper clientMapper, RowHmacGenerator rowHmacGenerator) {
1✔
239
      this.jooq = jooq;
1✔
240
      this.readonlyJooq = readonlyJooq;
1✔
241
      this.clientMapper = clientMapper;
1✔
242
      this.rowHmacGenerator = rowHmacGenerator;
1✔
243
    }
1✔
244

245
    @Override public ClientDAO readwrite() {
246
      return new ClientDAO(jooq, clientMapper, rowHmacGenerator);
1✔
247
    }
248

249
    @Override public ClientDAO readonly() {
250
      return new ClientDAO(readonlyJooq, clientMapper, rowHmacGenerator);
1✔
251
    }
252

253
    @Override public ClientDAO using(Configuration configuration) {
254
      DSLContext dslContext = DSL.using(checkNotNull(configuration));
1✔
255
      return new ClientDAO(dslContext, clientMapper, rowHmacGenerator);
1✔
256
    }
257
  }
258
}
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