diff --git a/api/src/main/java/keywhiz/api/automation/v2/SecretContentsRequestV2.java b/api/src/main/java/keywhiz/api/automation/v2/SecretContentsRequestV2.java new file mode 100644 index 000000000..368e9e2c0 --- /dev/null +++ b/api/src/main/java/keywhiz/api/automation/v2/SecretContentsRequestV2.java @@ -0,0 +1,38 @@ +package keywhiz.api.automation.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; + +@AutoValue public abstract class SecretContentsRequestV2 { + SecretContentsRequestV2() {} // prevent sub-classing + + + public static Builder builder() { + return new AutoValue_SecretContentsRequestV2.Builder() + .secrets(); + } + + @AutoValue.Builder public abstract static class Builder { + // intended to be package-private + abstract SecretContentsRequestV2.Builder secrets(ImmutableSet secrets); + + public SecretContentsRequestV2.Builder secrets(String... secrets) { + return secrets(ImmutableSet.copyOf(secrets)); + } + + public abstract SecretContentsRequestV2 build(); + } + + /** + * Static factory method used by Jackson for deserialization + */ + @SuppressWarnings("unused") + @JsonCreator public static SecretContentsRequestV2 fromParts( + @JsonProperty("secrets") ImmutableSet secrets) { + return builder().secrets(secrets).build(); + } + + @JsonProperty("secrets") public abstract ImmutableSet secrets(); +} diff --git a/api/src/main/java/keywhiz/api/automation/v2/SecretContentsResponseV2.java b/api/src/main/java/keywhiz/api/automation/v2/SecretContentsResponseV2.java new file mode 100644 index 000000000..2fca39c2d --- /dev/null +++ b/api/src/main/java/keywhiz/api/automation/v2/SecretContentsResponseV2.java @@ -0,0 +1,49 @@ +package keywhiz.api.automation.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; + +@AutoValue public abstract class SecretContentsResponseV2 { + SecretContentsResponseV2() {} // prevent sub-classing + + + public static Builder builder() { + return new AutoValue_SecretContentsResponseV2.Builder() + .successSecrets(ImmutableMap.of()) + .missingSecrets(ImmutableList.of()); + } + + @AutoValue.Builder public abstract static class Builder { + // intended to be package-private + abstract SecretContentsResponseV2.Builder successSecrets(ImmutableMap successSecrets); + abstract SecretContentsResponseV2.Builder missingSecrets(ImmutableList missingSecrets); + + + public SecretContentsResponseV2.Builder successSecrets(Map successSecrets) { + return successSecrets(ImmutableMap.copyOf(successSecrets)); + } + public SecretContentsResponseV2.Builder missingSecrets(List missingSecrets) { + return missingSecrets(ImmutableList.copyOf(missingSecrets)); + } + + public abstract SecretContentsResponseV2 build(); + } + + /** + * Static factory method used by Jackson for deserialization + */ + @SuppressWarnings("unused") + @JsonCreator public static SecretContentsResponseV2 fromParts( + @JsonProperty("successSecrets") ImmutableMap successSecrets, + @JsonProperty("missingSecrets") ImmutableList missingSecrets) { + return builder().successSecrets(successSecrets).missingSecrets(missingSecrets).build(); + } + + @JsonProperty("successSecrets") public abstract ImmutableMap successSecrets(); + @JsonProperty("missingSecrets") public abstract ImmutableList missingSecrets(); +} diff --git a/api/src/test/java/keywhiz/api/automation/v2/SecretContentsRequestV2Test.java b/api/src/test/java/keywhiz/api/automation/v2/SecretContentsRequestV2Test.java new file mode 100644 index 000000000..8273a1857 --- /dev/null +++ b/api/src/test/java/keywhiz/api/automation/v2/SecretContentsRequestV2Test.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package keywhiz.api.automation.v2; + +import org.junit.Test; + +import static keywhiz.testing.JsonHelpers.fromJson; +import static keywhiz.testing.JsonHelpers.jsonFixture; +import static org.assertj.core.api.Assertions.assertThat; + +public class SecretContentsRequestV2Test { + @Test public void deserializesCorrectly() throws Exception { + SecretContentsRequestV2 secretContentsRequest = SecretContentsRequestV2.builder() + .secrets("secret1", "secret2", "secret3") + .build(); + + assertThat(fromJson( + jsonFixture("fixtures/v2/secretContentsRequest.json"), SecretContentsRequestV2.class)) + .isEqualTo(secretContentsRequest); + } +} diff --git a/api/src/test/java/keywhiz/api/automation/v2/SecretContentsResponseV2Test.java b/api/src/test/java/keywhiz/api/automation/v2/SecretContentsResponseV2Test.java new file mode 100644 index 000000000..4a8e502af --- /dev/null +++ b/api/src/test/java/keywhiz/api/automation/v2/SecretContentsResponseV2Test.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package keywhiz.api.automation.v2; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; + +import static keywhiz.testing.JsonHelpers.fromJson; +import static keywhiz.testing.JsonHelpers.jsonFixture; +import static org.assertj.core.api.Assertions.assertThat; + +public class SecretContentsResponseV2Test { + @Test public void deserializesCorrectly() throws Exception { + SecretContentsResponseV2 secretContentsResponse = SecretContentsResponseV2.builder() + .successSecrets(ImmutableMap.of("secret1", "supersecretcontent1", "secret2", "supersecretcontent2")) + .missingSecrets(ImmutableList.of("secret3")) + .build(); + + assertThat(fromJson( + jsonFixture("fixtures/v2/secretContentsResponse.json"), SecretContentsResponseV2.class)) + .isEqualTo(secretContentsResponse); + } +} diff --git a/api/src/test/resources/fixtures/v2/secretContentsRequest.json b/api/src/test/resources/fixtures/v2/secretContentsRequest.json new file mode 100644 index 000000000..2bc32ecf8 --- /dev/null +++ b/api/src/test/resources/fixtures/v2/secretContentsRequest.json @@ -0,0 +1,3 @@ +{ + "secrets": ["secret1", "secret2", "secret3"] +} \ No newline at end of file diff --git a/api/src/test/resources/fixtures/v2/secretContentsResponse.json b/api/src/test/resources/fixtures/v2/secretContentsResponse.json new file mode 100644 index 000000000..f5ef1e180 --- /dev/null +++ b/api/src/test/resources/fixtures/v2/secretContentsResponse.json @@ -0,0 +1,7 @@ +{ + "successSecrets": { + "secret1" : "supersecretcontent1", + "secret2": "supersecretcontent2" + }, + "missingSecrets": ["secret3"] +} \ No newline at end of file diff --git a/log/src/main/java/keywhiz/log/EventTag.java b/log/src/main/java/keywhiz/log/EventTag.java index 56ba96d45..629e09027 100644 --- a/log/src/main/java/keywhiz/log/EventTag.java +++ b/log/src/main/java/keywhiz/log/EventTag.java @@ -10,6 +10,7 @@ public enum EventTag { SECRET_CHANGEVERSION, SECRET_DELETE, SECRET_BACKFILLEXPIRY, + SECRET_READCONTENT, GROUP_CREATE, GROUP_DELETE, diff --git a/server/src/main/java/keywhiz/service/resources/automation/v2/SecretResource.java b/server/src/main/java/keywhiz/service/resources/automation/v2/SecretResource.java index e8960ad98..4495cd33b 100644 --- a/server/src/main/java/keywhiz/service/resources/automation/v2/SecretResource.java +++ b/server/src/main/java/keywhiz/service/resources/automation/v2/SecretResource.java @@ -6,6 +6,7 @@ import com.google.common.collect.Sets; import io.dropwizard.auth.Auth; import java.time.Instant; +import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -33,6 +34,8 @@ import keywhiz.api.automation.v2.CreateSecretRequestV2; import keywhiz.api.automation.v2.ModifyGroupsRequestV2; import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2; +import keywhiz.api.automation.v2.SecretContentsRequestV2; +import keywhiz.api.automation.v2.SecretContentsResponseV2; import keywhiz.api.automation.v2.SecretDetailResponseV2; import keywhiz.api.automation.v2.SetSecretVersionRequestV2; import keywhiz.api.model.AutomationClient; @@ -481,6 +484,49 @@ public SecretDetailResponseV2 secretInfo(@Auth AutomationClient automationClient .build(); } + /** + * Retrieve contents for a set of secret series. Throws an exception + * for unexpected errors (i. e. empty secret names or errors connecting to + * the database); returns a response containing the contents of found + * secrets and a list of any missing secrets. + * + * @excludeParams automationClient + * + * @responseMessage 200 Secret series information retrieved + */ + @Timed @ExceptionMetered + @POST + @Path("contents") + @Produces(APPLICATION_JSON) + public SecretContentsResponseV2 secretContents(@Auth AutomationClient automationClient, + @Valid SecretContentsRequestV2 request) { + HashMap successSecrets = new HashMap<>(); + ArrayList missingSecrets = new ArrayList<>(); + + // Get the contents for each secret, recording any errors + for (String secretName : request.secrets()) { + // Get the secret, if present + Optional secret = secretController.getSecretByName(secretName); + + if (!secret.isPresent()) { + missingSecrets.add(secretName); + } else { + successSecrets.put(secretName, secret.get().getSecret()); + } + } + + // Record the read in the audit log, tracking which secrets were found and not found + Map extraInfo = new HashMap<>(); + extraInfo.put("success_secrets", successSecrets.keySet().toString()); + extraInfo.put("missing_secrets", missingSecrets.toString()); + auditLog.recordEvent(new Event(Instant.now(), EventTag.SECRET_READCONTENT, automationClient.getName(), request.secrets().toString(), extraInfo)); + + return SecretContentsResponseV2.builder() + .successSecrets(successSecrets) + .missingSecrets(missingSecrets) + .build(); + } + /** * Retrieve the given range of versions of this secret, sorted from newest to * oldest update time. If versionIdx is nonzero, then numVersions versions, diff --git a/server/src/test/java/keywhiz/service/resources/automation/v2/SecretResourceTest.java b/server/src/test/java/keywhiz/service/resources/automation/v2/SecretResourceTest.java index 539dc8f82..1bb6bae72 100644 --- a/server/src/test/java/keywhiz/service/resources/automation/v2/SecretResourceTest.java +++ b/server/src/test/java/keywhiz/service/resources/automation/v2/SecretResourceTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import io.dropwizard.jackson.Jackson; import java.io.IOException; @@ -12,6 +13,7 @@ import java.util.Base64.Encoder; import java.util.List; import java.util.Optional; +import java.util.Set; import keywhiz.IntegrationTestRule; import keywhiz.KeywhizService; import keywhiz.TestClients; @@ -20,6 +22,8 @@ import keywhiz.api.automation.v2.CreateSecretRequestV2; import keywhiz.api.automation.v2.ModifyGroupsRequestV2; import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2; +import keywhiz.api.automation.v2.SecretContentsRequestV2; +import keywhiz.api.automation.v2.SecretContentsResponseV2; import keywhiz.api.automation.v2.SecretDetailResponseV2; import keywhiz.api.automation.v2.SetSecretVersionRequestV2; import keywhiz.api.model.Group; @@ -200,6 +204,44 @@ public void partialUpdateSecret_notFound() throws Exception { assertThat(response.metadata()).isEqualTo(ImmutableMap.of("owner", "root", "mode", "0440")); } + //--------------------------------------------------------------------------------------- + // secretContents + //--------------------------------------------------------------------------------------- + + @Test public void secretContents_empty() throws Exception { + // No error expected when the list of requested secrets is empty + SecretContentsResponseV2 resp = contents(SecretContentsRequestV2.fromParts(ImmutableSet.of())); + assertThat(resp.successSecrets().isEmpty()).isTrue(); + assertThat(resp.missingSecrets().isEmpty()).isTrue(); + } + + @Test public void secretContents_success() throws Exception { + // Sample secrets + create(CreateSecretRequestV2.builder() + .name("secret23a") + .content(encoder.encodeToString("supa secret23a".getBytes(UTF_8))) + .description("desc") + .metadata(ImmutableMap.of("owner", "root", "mode", "0440")) + .type("password") + .build()); + + create(CreateSecretRequestV2.builder() + .name("secret23b") + .content(encoder.encodeToString("supa secret23b".getBytes(UTF_8))) + .description("desc") + .build()); + + SecretContentsRequestV2 request = SecretContentsRequestV2.fromParts( + ImmutableSet.of("secret23a", "secret23b", "non-existent") + ); + SecretContentsResponseV2 response = contents(request); + assertThat(response.successSecrets()).isEqualTo(ImmutableMap.of("secret23a", + encoder.encodeToString("supa secret23a".getBytes(UTF_8)), + "secret23b", encoder.encodeToString("supa secret23b".getBytes(UTF_8)))); + assertThat(response.missingSecrets()).isEqualTo(ImmutableList.of("non-existent")); + } + + //--------------------------------------------------------------------------------------- // secretGroupsListing //--------------------------------------------------------------------------------------- @@ -851,6 +893,14 @@ SecretDetailResponseV2 lookup(String name) throws IOException { return mapper.readValue(httpResponse.body().byteStream(), SecretDetailResponseV2.class); } + SecretContentsResponseV2 contents(SecretContentsRequestV2 request) throws IOException { + RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request)); + Request get = clientRequest("/automation/v2/secrets/contents").post(body).build(); + Response httpResponse = mutualSslClient.newCall(get).execute(); + assertThat(httpResponse.code()).isEqualTo(200); + return mapper.readValue(httpResponse.body().byteStream(), SecretContentsResponseV2.class); + } + List groupsListing(String name) throws IOException { Request get = clientRequest(format("/automation/v2/secrets/%s/groups", name)).get().build(); Response httpResponse = mutualSslClient.newCall(get).execute();