package keywhiz.service.resources.automation.v2;
import com.fasterxml.jackson.core.type.TypeReference;
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;
import java.net.URI;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.List;
import java.util.Optional;
import keywhiz.IntegrationTestRule;
import keywhiz.KeywhizService;
import keywhiz.TestClients;
import keywhiz.api.automation.v2.CreateGroupRequestV2;
import keywhiz.api.automation.v2.CreateOrUpdateSecretRequestV2;
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;
import keywhiz.api.model.SanitizedSecret;
import keywhiz.api.model.SanitizedSecretWithGroups;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.rules.RuleChain;
import static java.lang.String.format;
import static java.lang.Thread.sleep;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.HttpHeaders.LOCATION;
import static keywhiz.TestClients.clientRequest;
import static keywhiz.client.KeywhizClient.JSON;
import static org.assertj.core.api.Assertions.assertThat;
public class SecretResourceTest {
private static final ObjectMapper mapper =
KeywhizService.customizeObjectMapper(Jackson.newObjectMapper());
private static final Encoder encoder = Base64.getEncoder();
OkHttpClient mutualSslClient;
@ClassRule public static final RuleChain chain = IntegrationTestRule.rule();
@Before public void setUp() {
mutualSslClient = TestClients.mutualSslClient();
}
//---------------------------------------------------------------------------------------
// createSecret
//---------------------------------------------------------------------------------------
@Test public void createSecret_successUnVersioned() throws Exception {
CreateSecretRequestV2 request = CreateSecretRequestV2.builder()
.name("secret1")
.content(encoder.encodeToString("supa secret".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build();
Response httpResponse = create(request);
assertThat(httpResponse.code()).isEqualTo(201);
URI location = URI.create(httpResponse.header(LOCATION));
assertThat(location.getPath()).isEqualTo("/automation/v2/secrets/secret1");
}
@Test public void createSecret_duplicateUnVersioned() throws Exception {
CreateSecretRequestV2 request = CreateSecretRequestV2.builder()
.name("secret2")
.content(encoder.encodeToString("supa secret2".getBytes(UTF_8)))
.description("desc")
.build();
Response httpResponse = create(request);
assertThat(httpResponse.code()).isEqualTo(201);
httpResponse = create(request);
assertThat(httpResponse.code()).isEqualTo(409);
}
//---------------------------------------------------------------------------------------
// createOrUpdateSecret
//---------------------------------------------------------------------------------------
@Test public void createOrUpdateSecret() throws Exception {
CreateOrUpdateSecretRequestV2 request = CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString("supa secret".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build();
Response httpResponse = createOrUpdate(request, "secret3");
assertThat(httpResponse.code()).isEqualTo(201);
URI location = URI.create(httpResponse.header(LOCATION));
assertThat(location.getPath()).isEqualTo("/automation/v2/secrets/secret3");
httpResponse = createOrUpdate(request, "secret3");
assertThat(httpResponse.code()).isEqualTo(201);
location = URI.create(httpResponse.header(LOCATION));
assertThat(location.getPath()).isEqualTo("/automation/v2/secrets/secret3");
}
//---------------------------------------------------------------------------------------
@Ignore
@Test public void modifySecretSeries_notFound() throws Exception {
// TODO: need request object
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(null));
Request post = clientRequest("/automation/v2/secrets/non-existent").post(body).build();
Response httpResponse = mutualSslClient.newCall(post).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Ignore
@Test public void modifySecretSeries_success() throws Exception {
// secret5
// TODO: check different metadata, name, location
}
//---------------------------------------------------------------------------------------
// partialUpdateSecret
//---------------------------------------------------------------------------------------
@Test
public void partialUpdateSecret_success() throws Exception {
// Create a secret to update
CreateOrUpdateSecretRequestV2 createRequest = CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString("supa secret".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build();
Response httpResponse = createOrUpdate(createRequest, "secret3");
assertThat(httpResponse.code()).isEqualTo(201);
URI location = URI.create(httpResponse.header(LOCATION));
assertThat(location.getPath()).isEqualTo("/automation/v2/secrets/secret3");
// Update the secret's description and set its expiry
PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder()
.description("a more detailed description")
.descriptionPresent(true)
.expiry(1487268151L)
.expiryPresent(true)
.build();
httpResponse = partialUpdate(request, "secret3");
assertThat(httpResponse.code()).isEqualTo(201);
location = URI.create(httpResponse.header(LOCATION));
assertThat(location.getPath()).isEqualTo("/automation/v2/secrets/secret3");
}
@Test
public void partialUpdateSecret_notFound() throws Exception {
PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder()
.description("a more detailed description")
.descriptionPresent(true)
.expiry(1487268151L)
.expiryPresent(true)
.build();
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request post = clientRequest("/automation/v2/secrets/non-existent/partialupdate").post(body).build();
Response httpResponse = mutualSslClient.newCall(post).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
//---------------------------------------------------------------------------------------
// secretInfo
//---------------------------------------------------------------------------------------
@Test public void secretInfo_notFound() throws Exception {
Request get = clientRequest("/automation/v2/secrets/non-existent").get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Test public void secretInfo_success() throws Exception {
// Sample secret
create(CreateSecretRequestV2.builder()
.name("secret6")
.content(encoder.encodeToString("supa secret6".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build());
SecretDetailResponseV2 response = lookup("secret6");
assertThat(response.name()).isEqualTo("secret6");
assertThat(response.createdBy()).isEqualTo("client");
assertThat(response.updatedBy()).isEqualTo("client");
assertThat(response.createdAtSeconds()).isEqualTo(response.updatedAtSeconds());
assertThat(response.description()).isEqualTo("desc");
assertThat(response.type()).isEqualTo("password");
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
//---------------------------------------------------------------------------------------
@Test public void secretGroupsListing_notFound() throws Exception {
Request get = clientRequest("/automation/v2/secrets/non-existent/groups").get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Test public void secretGroupsListing_success() throws Exception {
createGroup("group7a");
createGroup("group7b");
// Sample secret
create(CreateSecretRequestV2.builder()
.name("secret7")
.content(encoder.encodeToString("supa secret7".getBytes(UTF_8)))
.groups("group7a", "group7b")
.build());
assertThat(groupsListing("secret7")).containsOnly("group7a", "group7b");
}
//---------------------------------------------------------------------------------------
// modifySecretGroups
//---------------------------------------------------------------------------------------
@Test public void modifySecretGroups_notFound() throws Exception {
ModifyGroupsRequestV2 request = ModifyGroupsRequestV2.builder().build();
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request put = clientRequest("/automation/v2/secrets/non-existent/groups").put(body).build();
Response httpResponse = mutualSslClient.newCall(put).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Test public void modifySecretGroups_success() throws Exception {
// Create sample secret and groups
createGroup("group8a");
createGroup("group8b");
createGroup("group8c");
create(CreateSecretRequestV2.builder()
.name("secret8")
.content(encoder.encodeToString("supa secret8".getBytes(UTF_8)))
.groups("group8a", "group8b")
.build());
// Modify secret
ModifyGroupsRequestV2 request = ModifyGroupsRequestV2.builder()
.addGroups("group8c", "non-existent1")
.removeGroups("group8a", "non-existent2")
.build();
List<String> groups = modifyGroups("secret8", request);
assertThat(groups).containsOnly("group8b", "group8c");
}
//---------------------------------------------------------------------------------------
// deleteSecretSeries
//---------------------------------------------------------------------------------------
@Test public void deleteSecretSeries_notFound() throws Exception {
assertThat(deleteSeries("non-existent").code()).isEqualTo(404);
}
@Test public void deleteSecretSeries_success() throws Exception {
// Sample secret
create(CreateSecretRequestV2.builder()
.name("secret12")
.content(encoder.encodeToString("supa secret12".getBytes(UTF_8)))
.build());
createGroup("testGroup");
ModifyGroupsRequestV2 request = ModifyGroupsRequestV2.builder()
.addGroups("testGroup", "secret12")
.build();
modifyGroups("secret12", request);
// Delete works
assertThat(deleteSeries("secret12").code()).isEqualTo(204);
// Subsequent deletes can't find the secret series
assertThat(deleteSeries("secret12").code()).isEqualTo(404);
}
//---------------------------------------------------------------------------------------
// secretListing
//---------------------------------------------------------------------------------------
@Test public void secretListing_success() throws Exception {
// Listing without secret16
assertThat(listing()).doesNotContain("secret16");
// Sample secret
create(CreateSecretRequestV2.builder()
.name("secret16")
.description("test secret 16")
.content(encoder.encodeToString("supa secret16".getBytes(UTF_8)))
.build());
// Listing with secret16
assertThat(listing()).contains("secret16");
List<SanitizedSecret> secrets = listingV2();
boolean found = false;
for (SanitizedSecret s : secrets) {
if (s.name().equals("secret16")) {
found = true;
assertThat(s.description()).isEqualTo("test secret 16");
}
}
assertThat(found).isTrue();
}
@Test public void secretListingBatch_success() throws Exception {
// Listing without secret23, 24, 25
String name1 = "secret23";
String name2 = "secret24";
String name3 = "secret25";
List<String> s = listing();
assertThat(s).doesNotContain(name1);
assertThat(s).doesNotContain(name2);
assertThat(s).doesNotContain(name3);
// create groups
createGroup("group16a");
createGroup("group16b");
// get current time to calculate timestamps off for expiry
long now = System.currentTimeMillis() / 1000L;
// add some secrets
create(CreateSecretRequestV2.builder()
.name(name1)
.content(encoder.encodeToString("supa secret17".getBytes(UTF_8)))
.expiry(now + 86400 * 3)
.groups("group16a", "group16b")
.build());
create(CreateSecretRequestV2.builder()
.name(name2)
.content(encoder.encodeToString("supa secret18".getBytes(UTF_8)))
.expiry(now + 86400)
.groups("group16a")
.build());
create(CreateSecretRequestV2.builder()
.name(name3)
.content(encoder.encodeToString("supa secret19".getBytes(UTF_8)))
.expiry(now + 86400 * 2)
.groups("group16b")
.build());
// check limiting by batch (hard to test because the batch results heavily depend on other
// tests, which may be run in parallel and often execute fast enough that different tests'
// secrets have the same creation time as the secrets created in this test)
List<String> s1 = listBatch(0, 2, false);
assertThat(s1.size()).isEqualTo(2);
List<SanitizedSecret> s3 = listBatchV2(0, 2, true);
assertThat(s3.size()).isEqualTo(2);
}
@Test public void secretListingBatch_failure() throws Exception {
// check that negative inputs fail
Request get = clientRequest(String.format("/automation/v2/secrets?idx=%d&num=%d&newestFirst=%s", -1, 3, false)).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(400);
get = clientRequest(String.format("/automation/v2/secrets?idx=%d&num=%d&newestFirst=%s", 0, -3, true)).get().build();
httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(400);
get = clientRequest(String.format("/automation/v2/secrets/v2?idx=%d&num=%d&newestFirst=%s", -1, 3, false)).get().build();
httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(400);
get = clientRequest(String.format("/automation/v2/secrets/v2?idx=%d&num=%d&newestFirst=%s", 0, -3, true)).get().build();
httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(400);
}
//---------------------------------------------------------------------------------------
// backfillExpiration
//---------------------------------------------------------------------------------------
@Test
public void backfillExpirationTest() throws Exception {
byte[] certs = Resources.toByteArray(Resources.getResource("fixtures/expiring-certificates.crt"));
byte[] pubring = Resources.toByteArray(Resources.getResource("fixtures/expiring-pubring.gpg"));
byte[] p12 = Resources.toByteArray(Resources.getResource("fixtures/expiring-keystore.p12"));
byte[] jceks = Resources.toByteArray(Resources.getResource("fixtures/expiring-keystore.jceks"));
create(CreateSecretRequestV2.builder()
.name("certificate-chain.crt")
.content(encoder.encodeToString(certs))
.build());
create(CreateSecretRequestV2.builder()
.name("public-keyring.gpg")
.content(encoder.encodeToString(pubring))
.build());
create(CreateSecretRequestV2.builder()
.name("keystore.p12")
.content(encoder.encodeToString(p12))
.build());
create(CreateSecretRequestV2.builder()
.name("keystore.jceks")
.content(encoder.encodeToString(jceks))
.build());
Response response = backfillExpiration("certificate-chain.crt", ImmutableList.of());
assertThat(response.isSuccessful()).isTrue();
response = backfillExpiration("public-keyring.gpg", ImmutableList.of());
assertThat(response.isSuccessful()).isTrue();
response = backfillExpiration("keystore.p12", ImmutableList.of("password"));
assertThat(response.isSuccessful()).isTrue();
response = backfillExpiration("keystore.jceks", ImmutableList.of("password"));
assertThat(response.isSuccessful()).isTrue();
SecretDetailResponseV2 details = lookup("certificate-chain.crt");
assertThat(details.expiry()).isEqualTo(1501533950);
details = lookup("public-keyring.gpg");
assertThat(details.expiry()).isEqualTo(1536442365);
details = lookup("keystore.p12");
assertThat(details.expiry()).isEqualTo(1681596851);
details = lookup("keystore.jceks");
assertThat(details.expiry()).isEqualTo(1681596851);
}
//---------------------------------------------------------------------------------------
// secretListingExpiry
//---------------------------------------------------------------------------------------
@Test public void secretListingExpiry_success() throws Exception {
// Listing without secret17,18,19
List<String> s = listing();
assertThat(s).doesNotContain("secret17");
assertThat(s).doesNotContain("secret18");
assertThat(s).doesNotContain("secret19");
assertThat(s).doesNotContain("secret19a");
// create groups
createGroup("group15a");
createGroup("group15b");
// get current time to calculate timestamps off for expiry
long now = System.currentTimeMillis() / 1000L;
// add some secrets
create(CreateSecretRequestV2.builder()
.name("secret17")
.content(encoder.encodeToString("supa secret17".getBytes(UTF_8)))
.expiry(now + 86400 * 3)
.groups("group15a", "group15b")
.build());
create(CreateSecretRequestV2.builder()
.name("secret18")
.content(encoder.encodeToString("supa secret18".getBytes(UTF_8)))
.expiry(now + 86400)
.groups("group15a")
.build());
create(CreateSecretRequestV2.builder()
.name("secret19")
.content(encoder.encodeToString("supa secret19".getBytes(UTF_8)))
.expiry(now + 86400 * 2)
.groups("group15b")
.build());
create(CreateSecretRequestV2.builder()
.name("secret19a")
.content(encoder.encodeToString("supa secret19a".getBytes(UTF_8)))
.expiry(now + 86400 * 2)
.build());
// check limiting by group and expiry
List<String> s1 = listExpiring(now + 86400 * 4, "group15a");
assertThat(s1).contains("secret17");
assertThat(s1).contains("secret18");
List<String> s2 = listExpiring(now + 86400 * 4, "group15b");
assertThat(s2).contains("secret19");
assertThat(s2).doesNotContain("secret18");
List<String> s3 = listExpiring(now + 86400 * 2, null);
assertThat(s3).contains("secret18");
assertThat(s3).doesNotContain("secret17");
List<SanitizedSecret> s4 = listExpiringV2(now + 86400 * 2, null);
assertThat(s4).hasSize(3);
assertThat(s4.get(0).name()).isEqualTo("secret18");
assertThat(s4.get(0).expiry()).isEqualTo(now + 86400);
assertThat(s4.get(1).name()).isEqualTo("secret19");
assertThat(s4.get(1).expiry()).isEqualTo(now + 86400 * 2);
assertThat(s4.get(2).name()).isEqualTo("secret19a");
assertThat(s4.get(2).expiry()).isEqualTo(now + 86400 * 2);
List<SanitizedSecretWithGroups> s5 = listExpiringV3(now + 86400 * 2, null);
assertThat(s5).hasSize(3);
assertThat(s5.get(0).secret().name()).isEqualTo("secret18");
assertThat(s5.get(0).secret().expiry()).isEqualTo(now + 86400);
assertThat(s5.get(0).groups().stream().map(Group::getName).collect(toList())).containsExactly("group15a");
assertThat(s5.get(1).secret().name()).isEqualTo("secret19");
assertThat(s5.get(1).secret().expiry()).isEqualTo(now + 86400 * 2);
assertThat(s5.get(1).groups().stream().map(Group::getName).collect(toList())).containsExactly("group15b");
assertThat(s5.get(2).secret().name()).isEqualTo("secret19a");
assertThat(s5.get(2).secret().expiry()).isEqualTo(now + 86400 * 2);
assertThat(s5.get(2).groups()).isEmpty();
}
//---------------------------------------------------------------------------------------
// secretVersions
//---------------------------------------------------------------------------------------
@Test public void secretVersionListing_notFound() throws Exception {
Request put = clientRequest("/automation/v2/secrets/non-existent/versions/0-0").build();
Response httpResponse = mutualSslClient.newCall(put).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Test public void secretVersionListing_success() throws Exception {
int totalVersions = 6;
int sleepInterval = 1000; // Delay so secrets have different creation timestamps
List<SecretDetailResponseV2> versions;
assertThat(listing()).doesNotContain("secret20");
// get current time to calculate timestamps off for expiry
long now = System.currentTimeMillis() / 1000L;
// Create secrets 1 second apart, so that the order of the versions, which
// will be listed by creation time, is fixed
for (int i = 0; i < totalVersions; i++) {
createOrUpdate(CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString(format("supa secret20_v%d", i).getBytes(UTF_8)))
.description(format("secret20, version %d", i))
.expiry(now + 86400 * 2)
.metadata(ImmutableMap.of("version", Integer.toString(i)))
.build(), "secret20");
sleep(sleepInterval);
}
// List all versions of this secret
versions = listVersions("secret20", 0, 1000);
checkSecretVersions(versions, "secret20", totalVersions, 0, 1000);
// List the newest half of the versions of this secret
versions = listVersions("secret20", 0, totalVersions / 2);
checkSecretVersions(versions, "secret20", totalVersions, 0, totalVersions / 2);
// List the oldest half of the versions of this secret
versions = listVersions("secret20", totalVersions / 2, totalVersions);
checkSecretVersions(versions, "secret20", totalVersions, totalVersions / 2, totalVersions);
// List the middle half of the versions of this secret
versions = listVersions("secret20", totalVersions / 4, totalVersions / 2);
checkSecretVersions(versions, "secret20", totalVersions, totalVersions / 4,
totalVersions / 2);
}
//---------------------------------------------------------------------------------------
// resetSecretVersion
//---------------------------------------------------------------------------------------
@Test public void secretChangeVersion_notFound() throws Exception {
Request post =
clientRequest("/automation/v2/secrets/non-existent/setversion").post(
RequestBody.create(JSON, mapper.writeValueAsString(
SetSecretVersionRequestV2.builder().name("non-existent").version(0).build())))
.build();
Response httpResponse = mutualSslClient.newCall(post).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}
@Test public void secretChangeVersion_success() throws Exception {
int totalVersions = 6;
String name = "secret21";
List<SecretDetailResponseV2> versions;
SecretDetailResponseV2 initialCurrentVersion;
SecretDetailResponseV2 finalCurrentVersion;
assertThat(listing()).doesNotContain(name);
// get current time to calculate timestamps off for expiry
long now = System.currentTimeMillis() / 1000L;
// Create secrets
for (int i = 0; i < totalVersions; i++) {
createOrUpdate(CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString(format("supa secret21_v%d", i).getBytes(UTF_8)))
.description(format("%s, version %d", name, i))
.expiry(now + 86400 * 2)
.metadata(ImmutableMap.of("version", Integer.toString(i)))
.build(), name);
sleep(2000 / totalVersions);
}
// Get the current version (the last version created)
initialCurrentVersion = lookup(name);
assertThat(initialCurrentVersion.name()).isEqualTo(name);
assertThat(
initialCurrentVersion.description()).isEqualTo(format("%s, version %d", name, totalVersions - 1));
// Get the earliest version of this secret
versions = listVersions(name, totalVersions - 3, 1);
assertThat(versions.get(0)).isNotEqualTo(initialCurrentVersion);
// Reset the current version to this version
setCurrentVersion(
SetSecretVersionRequestV2.builder().name(name).version(versions.get(0).version()).build());
// Get the current version
finalCurrentVersion = lookup(name);
assertThat(finalCurrentVersion).isEqualToIgnoringGivenFields(versions.get(0), "updatedAtSeconds");
assertThat(finalCurrentVersion).isNotEqualTo(initialCurrentVersion);
}
@Test public void secretChangeVersion_invalidVersion() throws Exception {
int totalVersions = 3;
String name = "secret22";
List<SecretDetailResponseV2> versions;
SecretDetailResponseV2 initialCurrentVersion;
SecretDetailResponseV2 finalCurrentVersion;
assertThat(listing()).doesNotContain(name);
// get current time to calculate timestamps off for expiry
long now = System.currentTimeMillis() / 1000L;
// Create secrets
for (int i = 0; i < totalVersions; i++) {
createOrUpdate(CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString(format("supa secret22_v%d", i).getBytes(UTF_8)))
.description(format("%s, version %d", name, i))
.expiry(now + 86400 * 2)
.metadata(ImmutableMap.of("version", Integer.toString(i)))
.build(), name);
}
// Get the current version (the last version created)
initialCurrentVersion = lookup(name);
assertThat(initialCurrentVersion.name()).isEqualTo(name);
assertThat(initialCurrentVersion.description()).isEqualTo(format("%s, version %d", name, totalVersions - 1));
// Get an invalid version of this secret
versions = listVersions(name, 0, totalVersions);
Optional<Long> maxValidVersion = versions.stream().map(SecretDetailResponseV2::version).max(Long::compare);
if (maxValidVersion.isPresent()) {
// Reset the current version to this version
Request post = clientRequest(String.format("/automation/v2/secrets/%s/setversion", name)).post(
RequestBody.create(JSON, mapper.writeValueAsString(SetSecretVersionRequestV2.builder()
.name(name)
.version(maxValidVersion.get() + 1)
.build()))).build();
Response httpResponse = mutualSslClient.newCall(post).execute();
assertThat(httpResponse.code()).isEqualTo(400);
// Get the current version, which should not have changed
finalCurrentVersion = lookup(name);
assertThat(finalCurrentVersion).isEqualTo(initialCurrentVersion);
}
}
/**
* Iterates over the given list of secret versions to verify that they are sorted from most
* recent creation date to least recent, that they have the expected version numbers,
* and that they have the correct secret name.
*
* @param versions a list of information on versions of secrets
* @param name of the secret series
* @param totalVersions the number of versions created
* @param versionIdx the index in the overall version list of the newest version taken
* @param numVersions the maximum number of versions taken
*/
private void checkSecretVersions(List<SecretDetailResponseV2> versions, String name,
int totalVersions, int versionIdx, int numVersions) {
long creationTime = System.currentTimeMillis() / 1000L;
int startIdx = totalVersions - versionIdx - 1;
int expectedVersions = Math.min(numVersions, totalVersions - versionIdx);
// Check that we retrieved as many secrets as possible
assertThat(versions.size()).isEqualTo(expectedVersions);
for (SecretDetailResponseV2 version : versions) {
// Check creation ordering (createdAtSeconds is from the secret series; updatedAtSeconds
// is from the content and matches when this version was created)
assertThat(version.updatedAtSeconds()).isLessThan(creationTime);
creationTime = version.updatedAtSeconds();
// Check version number
assertThat(version.metadata()).isEqualTo(
ImmutableMap.of("version", Integer.toString(startIdx--)));
// Check secret name
assertThat(version.name()).isEqualTo(name);
}
}
//---------------------------------------------------------------------------------------
// Version handling after creation and deletion
//---------------------------------------------------------------------------------------
/**
* A test which verifies that when a secret is created and deleted, and another secret
* with the same name is created later, listing versions of the current secret
* does not include versions from the original secret.
*/
@Test public void secretVersionManagement_createAndDelete() throws Exception {
String name = "versionManagementSecret";
String firstDescription = "the first secret with this name";
String secondDescription = "the second secret with this name";
// Create a secret
create(CreateSecretRequestV2.builder()
.name(name)
.description(firstDescription)
.content(encoder.encodeToString("secret version 1".getBytes(UTF_8)))
.build());
// Check that the secret's current versions are as expected
List<SecretDetailResponseV2> versions = listVersions(name, 0, 1000);
assertThat(versions.size()).isEqualTo(1);
assertThat(versions.get(0).description()).isEqualTo(firstDescription);
// Delete the secret and recreate it
deleteSeries(name);
create(CreateSecretRequestV2.builder()
.name(name)
.description(secondDescription)
.content(encoder.encodeToString("secret version 2".getBytes(UTF_8)))
.build());
// Check that the original secret's versions were not retrieved
versions = listVersions(name, 0, 1000);
assertThat(versions.size()).isEqualTo(1);
assertThat(versions.get(0).description()).isEqualTo(secondDescription);
}
//---------------------------------------------------------------------------------------
// helper functions for tests
//---------------------------------------------------------------------------------------
private Response createGroup(String name) throws IOException {
GroupResourceTest groupResourceTest = new GroupResourceTest();
groupResourceTest.mutualSslClient = mutualSslClient;
return groupResourceTest.create(CreateGroupRequestV2.builder().name(name).build());
}
Response create(CreateSecretRequestV2 request) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request post = clientRequest("/automation/v2/secrets").post(body).build();
return mutualSslClient.newCall(post).execute();
}
Response backfillExpiration(String name, List<String> passwords) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(passwords));
Request post = clientRequest(String.format("/automation/v2/secrets/%s/backfill-expiration", name)).post(body).build();
return mutualSslClient.newCall(post).execute();
}
Response createOrUpdate(CreateOrUpdateSecretRequestV2 request, String name) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request post = clientRequest(format("/automation/v2/secrets/%s", name)).post(body).build();
return mutualSslClient.newCall(post).execute();
}
Response partialUpdate(PartialUpdateSecretRequestV2 request, String name) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request post = clientRequest(format("/automation/v2/secrets/%s/partialupdate", name)).post(body).build();
return mutualSslClient.newCall(post).execute();
}
List<String> listing() throws IOException {
Request get = clientRequest("/automation/v2/secrets").get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<String>>() {
});
}
List<String> listBatch(int idx, int num, boolean newestFirst) throws IOException {
Request get = clientRequest(String.format("/automation/v2/secrets?idx=%d&num=%d&newestFirst=%s", idx, num, newestFirst)).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<String>>() {
});
}
List<SanitizedSecret> listingV2() throws IOException {
Request get = clientRequest("/automation/v2/secrets/v2").get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<SanitizedSecret>>() {
});
}
List<SanitizedSecret> listBatchV2(int idx, int num, boolean newestFirst) throws IOException {
Request get = clientRequest(String.format("/automation/v2/secrets/v2?idx=%d&num=%d&newestFirst=%s", idx, num, newestFirst)).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<SanitizedSecret>>() {
});
}
List<String> listExpiring(Long time, String groupName) throws IOException {
String requestURL = "/automation/v2/secrets/expiring/";
if (time != null && time > 0) {
requestURL += time.toString() + "/";
}
if (groupName != null && groupName.length() > 0) {
requestURL += groupName;
}
Request get = clientRequest(requestURL).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<String>>() {
});
}
List<SanitizedSecret> listExpiringV2(Long time, String groupName) throws IOException {
String requestURL = "/automation/v2/secrets/expiring/v2/";
if (time != null && time > 0) {
requestURL += time.toString() + "/";
}
if (groupName != null && groupName.length() > 0) {
requestURL += groupName;
}
Request get = clientRequest(requestURL).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<SanitizedSecret>>() {
});
}
List<SanitizedSecretWithGroups> listExpiringV3(Long time, String groupName) throws IOException {
String requestURL = "/automation/v2/secrets/expiring/v3/";
if (time != null && time > 0) {
requestURL += time.toString() + "/";
}
if (groupName != null && groupName.length() > 0) {
requestURL += groupName;
}
Request get = clientRequest(requestURL).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<SanitizedSecretWithGroups>>() {
});
}
private List<SecretDetailResponseV2> listVersions(String name, int versionIdx, int numVersions)
throws IOException {
Request get = clientRequest(
format("/automation/v2/secrets/%s/versions?versionIdx=%d&numVersions=%d", name, versionIdx, numVersions)).get()
.build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(),
new TypeReference<List<SecretDetailResponseV2>>() {
});
}
private void setCurrentVersion(SetSecretVersionRequestV2 request)
throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request post =
clientRequest(format("/automation/v2/secrets/%s/setversion", request.name())).post(body)
.build();
Response httpResponse = mutualSslClient.newCall(post).execute();
assertThat(httpResponse.code()).isEqualTo(201);
}
SecretDetailResponseV2 lookup(String name) throws IOException {
Request get = clientRequest("/automation/v2/secrets/" + name).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
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/request/contents").post(body).build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), SecretContentsResponseV2.class);
}
List<String> groupsListing(String name) throws IOException {
Request get = clientRequest(format("/automation/v2/secrets/%s/groups", name)).get().build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<String>>() {
});
}
List<String> modifyGroups(String name, ModifyGroupsRequestV2 request) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request put = clientRequest(format("/automation/v2/secrets/%s/groups", name)).put(body).build();
Response httpResponse = mutualSslClient.newCall(put).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), new TypeReference<List<String>>() {
});
}
Response deleteSeries(String name) throws IOException {
Request delete = clientRequest("/automation/v2/secrets/" + name).delete().build();
return mutualSslClient.newCall(delete).execute();
}
}