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

square / keywhiz / 4487728008

pending completion
4487728008

Pull #1204

github

GitHub
Merge ccff7a429 into c0db24b8c
Pull Request #1204: BatchResource: Hoisting builder creation (and potential remote calls)…

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

5304 of 7062 relevant lines covered (75.11%)

0.75 hits per line

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

86.81
/server/src/main/java/keywhiz/service/resources/automation/v2/BatchResource.java
1
package keywhiz.service.resources.automation.v2;
2

3
import com.codahale.metrics.annotation.ExceptionMetered;
4
import com.codahale.metrics.annotation.Timed;
5
import io.dropwizard.auth.Auth;
6
import java.time.Instant;
7
import java.util.ArrayList;
8
import java.util.HashMap;
9
import java.util.Map;
10
import java.util.Optional;
11
import java.util.Set;
12
import java.util.function.Function;
13
import java.util.stream.Collectors;
14
import javax.inject.Inject;
15
import javax.validation.Valid;
16
import javax.ws.rs.Consumes;
17
import javax.ws.rs.POST;
18
import javax.ws.rs.Path;
19
import javax.ws.rs.core.MediaType;
20
import keywhiz.KeywhizConfig;
21
import keywhiz.api.automation.v2.BatchCreateOrUpdateSecretsRequestV2;
22
import keywhiz.api.automation.v2.BatchCreateOrUpdateSecretsResponseV2;
23
import keywhiz.api.automation.v2.BatchMode;
24
import keywhiz.api.automation.v2.CreateOrUpdateSecretInfoV2;
25
import keywhiz.api.model.AutomationClient;
26
import keywhiz.api.model.Group;
27
import keywhiz.api.model.Secret;
28
import keywhiz.api.model.SecretSeriesAndContent;
29
import keywhiz.log.AuditLog;
30
import keywhiz.log.Event;
31
import keywhiz.log.EventTag;
32
import keywhiz.log.LogArguments;
33
import keywhiz.service.daos.AclDAO;
34
import keywhiz.service.daos.SecretController;
35
import keywhiz.service.daos.SecretDAO;
36
import keywhiz.service.permissions.Action;
37
import keywhiz.service.permissions.PermissionCheck;
38
import org.jooq.DSLContext;
39
import org.jooq.impl.DSL;
40
import org.slf4j.Logger;
41
import org.slf4j.LoggerFactory;
42

43
@Path("/automation/v2/batch")
44
public class BatchResource {
45
  private static final Logger logger = LoggerFactory.getLogger(BatchResource.class);
1✔
46

47
  private final DSLContext jooq;
48
  private final SecretController secretController;
49
  private final AclDAO aclDAO;
50
  private final SecretDAO secretDAO;
51
  private final AuditLog auditLog;
52
  private final PermissionCheck permissionCheck;
53
  private final KeywhizConfig config;
54

55
  @Inject
56
  public BatchResource(
57
      DSLContext jooq,
58
      SecretController secretController,
59
      AclDAO.AclDAOFactory aclDAOFactory,
60
      SecretDAO.SecretDAOFactory secretDAOFactory,
61
      AuditLog auditLog,
62
      PermissionCheck permissionCheck,
63
      KeywhizConfig config) {
1✔
64
    this.jooq = jooq;
1✔
65
    this.secretController = secretController;
1✔
66
    this.aclDAO = aclDAOFactory.readonly();
1✔
67
    this.secretDAO = secretDAOFactory.readwrite();
1✔
68
    this.auditLog = auditLog;
1✔
69
    this.permissionCheck = permissionCheck;
1✔
70
    this.config = config;
1✔
71
  }
1✔
72

73
  @Timed
74
  @ExceptionMetered
75
  @Path("secrets")
76
  @POST
77
  @Consumes(MediaType.APPLICATION_JSON)
78
  @LogArguments
79
  public BatchCreateOrUpdateSecretsResponseV2 batchCreateOrUpdateSecrets(
80
      @Auth AutomationClient automationClient,
81
      @Valid BatchCreateOrUpdateSecretsRequestV2 request) {
82
    permissionCheck.checkAllowedForTargetTypeOrThrow(automationClient, Action.CREATE, Secret.class);
1✔
83

84
    switch (request.batchMode()) {
1✔
85
      case BatchMode.ALL_OR_NONE:
86
        Map<CreateOrUpdateSecretInfoV2, SecretController.SecretBuilder> secretsToBuilders = request.secrets()
1✔
87
            .stream()
1✔
88
            .collect(
1✔
89
                Collectors.toMap(
1✔
90
                    Function.identity(),
1✔
91
                    secret -> toBuilder(automationClient, secret)));
1✔
92
        jooq.transaction(configuration -> {
1✔
93
          DSLContext dslContext = DSL.using(configuration);
1✔
94
          for (Map.Entry<CreateOrUpdateSecretInfoV2, SecretController.SecretBuilder> entry : secretsToBuilders.entrySet()) {
1✔
95
            createOrUpdateSecret(dslContext, automationClient, entry.getKey(), entry.getValue());
1✔
96
          }
1✔
97
        });
1✔
98
        break;
1✔
99
      case BatchMode.BEST_EFFORT:
100
        for (CreateOrUpdateSecretInfoV2 secret : request.secrets()) {
1✔
101
          try {
102
            SecretController.SecretBuilder builder = toBuilder(automationClient, secret);
1✔
103
            jooq.transaction(configuration -> {
1✔
104
                DSLContext dslContext = DSL.using(configuration);
1✔
105
                createOrUpdateSecret(dslContext, automationClient, secret, builder);
1✔
106
            });
1✔
107
          } catch (Exception e) {
1✔
108
            logger.error(String.format("Failed to create or update secret: %s", secret), e);
1✔
109
          }
1✔
110
        }
1✔
111
        break;
1✔
112
      case BatchMode.FAIL_FAST:
113
        for (CreateOrUpdateSecretInfoV2 secret : request.secrets()) {
1✔
114
          SecretController.SecretBuilder builder = toBuilder(automationClient, secret);
1✔
115
          jooq.transaction(configuration -> {
1✔
116
            DSLContext dslContext = DSL.using(configuration);
1✔
117
            createOrUpdateSecret(dslContext, automationClient, secret, builder);
1✔
118
          });
1✔
119
        }
1✔
120
        break;
1✔
121
      default:
122
        throw new IllegalArgumentException(
1✔
123
            String.format("Unknown batch mode: %s", request.batchMode()));
1✔
124
    }
125

126
    return BatchCreateOrUpdateSecretsResponseV2.builder()
1✔
127
        .build();
1✔
128
  }
129

130
  private void createOrUpdateSecret(
131
      DSLContext dslContext,
132
      AutomationClient automationClient,
133
      CreateOrUpdateSecretInfoV2 secret,
134
      SecretController.SecretBuilder builder) {
135

136
    String secretOwner = secret.owner();
1✔
137
    {
138
      Optional<SecretSeriesAndContent> maybeSecretSeriesAndContent =
1✔
139
          secretDAO.getSecretByName(dslContext, secret.name());
1✔
140
      if (maybeSecretSeriesAndContent.isPresent()) {
1✔
141
        permissionCheck.checkAllowedOrThrow(automationClient, Action.UPDATE,
×
142
            maybeSecretSeriesAndContent.get());
×
143
      } else {
144
        permissionCheck.checkAllowedForTargetTypeOrThrow(automationClient, Action.CREATE,
1✔
145
            Secret.class);
146
        secretOwner = getSecretOwnerForSecretCreation(dslContext, secretOwner, automationClient);
1✔
147
      }
148
    }
149

150
    builder
1✔
151
        .withOwnerName(secretOwner)
1✔
152
        .createOrUpdate(dslContext);
1✔
153

154
    emitAuditLogEntry(automationClient, secret);
1✔
155
  }
1✔
156

157
  private SecretController.SecretBuilder toBuilder(
158
      AutomationClient automationClient,
159
      CreateOrUpdateSecretInfoV2 secret) {
160
    return secretController
1✔
161
        .builder(secret.name(), secret.content(), automationClient.getName(), secret.expiry())
1✔
162
        .withDescription(secret.description())
1✔
163
        .withMetadata(secret.metadata())
1✔
164
        .withType(secret.type());
1✔
165
  }
166

167
  private void emitAuditLogEntry(AutomationClient automationClient, CreateOrUpdateSecretInfoV2 secret) {
168
    Map<String, String> extraInfo = new HashMap<>();
1✔
169
    if (secret.description() != null) {
1✔
170
      extraInfo.put("description", secret.description());
1✔
171
    }
172
    if (secret.metadata() != null) {
1✔
173
      extraInfo.put("metadata", secret.metadata().toString());
1✔
174
    }
175
    extraInfo.put("expiry", Long.toString(secret.expiry()));
1✔
176
    auditLog.recordEvent(
1✔
177
        new Event(
178
            Instant.now(),
1✔
179
            EventTag.SECRET_CREATEORUPDATE,
180
            automationClient.getName(),
1✔
181
            secret.name(),
1✔
182
            extraInfo));
183
  }
1✔
184

185
  private String getSecretOwnerForSecretCreation(DSLContext dslContext, String secretOwner, AutomationClient automationClient) {
186
    if (secretOwnerNotProvided(secretOwner) && shouldInferSecretOwnerUponCreation()) {
1✔
187
      return findClientGroup(dslContext, automationClient);
×
188
    }
189
    return secretOwner;
1✔
190
  }
191

192
  private static boolean secretOwnerNotProvided(String secretOwner) {
193
    return secretOwner == null || secretOwner.isEmpty();
1✔
194
  }
195

196
  private boolean shouldInferSecretOwnerUponCreation() {
197
    return config.getNewSecretOwnershipStrategy() == KeywhizConfig.NewSecretOwnershipStrategy.INFER_FROM_CLIENT;
1✔
198
  }
199

200
  private String findClientGroup(DSLContext dslContext, AutomationClient automationClient) {
201
    Set<Group> clientGroups = aclDAO.getGroupsFor(dslContext, automationClient);
×
202
    if (clientGroups.size() == 0) {
×
203
      logger.warn(String.format("Client %s does not belong to any group.", automationClient));
×
204
    } else if (clientGroups.size() == 1) {
×
205
      Group clientGroup = clientGroups.stream().findFirst().get();
×
206
      return clientGroup.getName();
×
207
    } else {
208
      String groups = new ArrayList<>(clientGroups).toString();
×
209
      logger.warn(String.format("Client %s belongs to more than one group: %s",
×
210
          automationClient,
211
          groups));
212
    }
213
    return null;
×
214
  }
215
}
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