Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1215 from square/gchao/secret-contents-at-version
Browse files Browse the repository at this point in the history
add automation API to get secrets by name+version
  • Loading branch information
graysonchao committed Apr 20, 2023
2 parents 1a97181 + 2f7d7c7 commit 840b264
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -69,6 +69,11 @@ If you made changes to the database model and want to regenerate sources:

We recommend [IntelliJ IDEA](https://www.jetbrains.com/idea/) for development.

## IntelliJ IDEA

To enable auto-completion, code navigation, etc., open the `keywhiz` repository in IDEA,
right click `pom.xml` in the repository root, and select "Add as Maven Project".

## Clients & API

Square also maintains a Keywhiz client implementation called [Keysync](https://github.com/square/keysync).
Expand Down
@@ -0,0 +1,40 @@
package keywhiz.api.automation.v2;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;

@AutoValue public abstract class SecretContentsAtVersionRequestV2 {
SecretContentsAtVersionRequestV2() {} // prevent sub-classing


public static Builder builder() {
return new AutoValue_SecretContentsAtVersionRequestV2.Builder()
.secret("")
.version(0L);
}

@AutoValue.Builder public abstract static class Builder {
public abstract SecretContentsAtVersionRequestV2.Builder secret(String secrets);
public abstract SecretContentsAtVersionRequestV2.Builder version(Long version);

public abstract SecretContentsAtVersionRequestV2 build();
}

/**
* Static factory method used by Jackson for deserialization
*/
@SuppressWarnings("unused")
@JsonCreator public static SecretContentsAtVersionRequestV2 fromParts(
@JsonProperty("secret") String secret,
@JsonProperty("version") Long version
) {
return builder()
.secret(secret)
.version(version)
.build();
}

@JsonProperty("secret") public abstract String secret();
@JsonProperty("version") public abstract Long version();
}
@@ -0,0 +1,34 @@
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 SecretContentsAtVersionResponseV2 {
SecretContentsAtVersionResponseV2() {} // prevent sub-classing


public static Builder builder() {
return new AutoValue_SecretContentsAtVersionResponseV2.Builder().secret("");
}

@AutoValue.Builder public abstract static class Builder {
public abstract SecretContentsAtVersionResponseV2.Builder secret(String secret);
public abstract SecretContentsAtVersionResponseV2 build();
}

/**
* Static factory method used by Jackson for deserialization
*/
@SuppressWarnings("unused")
@JsonCreator public static SecretContentsAtVersionResponseV2 fromParts(
@JsonProperty("secret") String secret) {
return builder().secret(secret).build();
}

@JsonProperty("secret") public abstract String secret();
}
Expand Up @@ -80,6 +80,10 @@ public Optional<Secret> getSecretByName(String name) {
return secretDAO.getSecretByName(name).map(transformer::transform);
}

public Optional<Secret> getSecretByNameAndVersion(String name, Long version) {
return secretDAO.getSecretByNameAndVersion(name, version).map(transformer::transform);
}

/**
* @param names of secrets series to look up secrets by.
* @return all existing secrets matching criteria.
Expand Down
23 changes: 23 additions & 0 deletions server/src/main/java/keywhiz/service/daos/SecretDAO.java
Expand Up @@ -395,6 +395,29 @@ public Optional<SecretSeriesAndContent> getSecretByName(DSLContext dslContext, S
return Optional.empty();
}

/**
* @param name of secret series to look up secrets by.
* @param version of secret contents to return.
* @return Secret matching input parameters or Optional.empty().
*/
public Optional<SecretSeriesAndContent> getSecretByNameAndVersion(String name, Long version) {
return getSecretByNameAndVersion(dslContext, name, version);
}

public Optional<SecretSeriesAndContent> getSecretByNameAndVersion(DSLContext dslContext, String name, Long version) {
checkArgument(!name.isEmpty());
SecretContentDAO secretContentDAO = secretContentDAOFactory.using(dslContext.configuration());
SecretSeriesDAO secretSeriesDAO = secretSeriesDAOFactory.using(dslContext.configuration());

Optional<SecretSeries> series = secretSeriesDAO.getSecretSeriesByName(name);
if (series.isPresent()) {
Optional<SecretContent> secretContent =
secretContentDAO.getSecretContentById(version);
return secretContent.map(content -> SecretSeriesAndContent.of(series.get(), content));
}
return Optional.empty();
}

/**
* @param names of secrets series to look up secrets by.
* @return Secrets matching input parameters.
Expand Down
Expand Up @@ -38,6 +38,8 @@
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
import keywhiz.api.automation.v2.SecretContentsRequestV2;
import keywhiz.api.automation.v2.SecretContentsResponseV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionRequestV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionResponseV2;
import keywhiz.api.automation.v2.SecretDetailResponseV2;
import keywhiz.api.automation.v2.SetSecretVersionRequestV2;
import keywhiz.api.model.AutomationClient;
Expand Down Expand Up @@ -685,6 +687,27 @@ public SecretContentsResponseV2 secretContents(@Auth AutomationClient automation
.build();
}

/**
* Retrieve a particular version of just one secret.
*/
@Timed @ExceptionMetered
@POST
@Path("request/contents-at-version")
@Produces(APPLICATION_JSON)
@LogArguments
public SecretContentsAtVersionResponseV2 secretContentsAtVersion(@Auth AutomationClient automationClient, @Valid SecretContentsAtVersionRequestV2 request) {
Secret secret = secretController.getSecretByNameAndVersion(request.secret(), request.version())
.orElseThrow(NotFoundException::new);
permissionCheck.checkAllowedOrThrow(automationClient, Action.READ, secret);

// Record the read in the audit log, tracking which secrets were found and not found
auditLog.recordEvent(new Event(Instant.now(), EventTag.SECRET_READCONTENT, automationClient.getName(), request.secret(), Map.of()));

return SecretContentsAtVersionResponseV2.builder()
.secret(secret.getSecret())
.build();
}

/**
* Retrieve the given range of versions of this secret, sorted from newest to
* oldest update time. If versionIdx is nonzero, then numVersions versions,
Expand Down
Expand Up @@ -25,6 +25,8 @@
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
import keywhiz.api.automation.v2.SecretContentsRequestV2;
import keywhiz.api.automation.v2.SecretContentsResponseV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionRequestV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionResponseV2;
import keywhiz.api.automation.v2.SecretDetailResponseV2;
import keywhiz.api.automation.v2.SetSecretVersionRequestV2;
import keywhiz.api.model.SanitizedSecret;
Expand Down Expand Up @@ -448,6 +450,65 @@ public void partialUpdateSecret_notFound() throws Exception {
}


//---------------------------------------------------------------------------------------
// secretContentsAtVersion
//---------------------------------------------------------------------------------------

@Test public void secretContentsAtVersion_success() throws Exception {
// Sample secrets
secretResourceTestHelper.create(CreateSecretRequestV2.builder()
.name("secret29")
.content(encoder.encodeToString("top secret29".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build());

Long oldVersion = secretResourceTestHelper.getSecret("secret29").orElseThrow().version();

secretResourceTestHelper.createOrUpdate(CreateOrUpdateSecretRequestV2.builder()
.content(encoder.encodeToString("rotated secret29".getBytes(UTF_8)))
.description("updated description")
.build(), "secret29");

SecretContentsAtVersionRequestV2 request = SecretContentsAtVersionRequestV2.fromParts(
"secret29", oldVersion
);
SecretContentsAtVersionResponseV2 response = secretResourceTestHelper.contentsAtVersion(request);
assertThat(response.secret()).isEqualTo(encoder.encodeToString("top secret29".getBytes(UTF_8)));
}

@Test public void secretContentsAtVersion_notFound() throws Exception {
// Sample secrets
secretResourceTestHelper.create(CreateSecretRequestV2.builder()
.name("secret30")
.content(encoder.encodeToString("top secret30".getBytes(UTF_8)))
.description("desc")
.metadata(ImmutableMap.of("owner", "root", "mode", "0440"))
.type("password")
.build());

// Request a version that we know does not exist for this secret name.
Long version = secretResourceTestHelper.getSecret("secret30").orElseThrow().version();
SecretContentsAtVersionRequestV2 request = SecretContentsAtVersionRequestV2.fromParts(
"secret30", version + 1
);

RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request get = clientRequest("/automation/v2/secrets/request/contents-at-version").post(body).build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(404);

// Request the right version, but the wrong secret name.
SecretContentsAtVersionRequestV2 request2 = SecretContentsAtVersionRequestV2.fromParts(
"DEFINITELYnotTHEsecret", version
);
body = RequestBody.create(JSON, mapper.writeValueAsString(request2));
get = clientRequest("/automation/v2/secrets/request/contents-at-version").post(body).build();
httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(404);
}

//---------------------------------------------------------------------------------------
// secretGroupsListing
//---------------------------------------------------------------------------------------
Expand Down
Expand Up @@ -14,6 +14,8 @@
import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2;
import keywhiz.api.automation.v2.SecretContentsRequestV2;
import keywhiz.api.automation.v2.SecretContentsResponseV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionRequestV2;
import keywhiz.api.automation.v2.SecretContentsAtVersionResponseV2;
import keywhiz.api.automation.v2.SecretDetailResponseV2;
import keywhiz.api.model.SanitizedSecret;
import keywhiz.api.model.SanitizedSecretWithGroups;
Expand Down Expand Up @@ -251,6 +253,14 @@ SecretContentsResponseV2 contents(SecretContentsRequestV2 request) throws IOExce
return mapper.readValue(httpResponse.body().byteStream(), SecretContentsResponseV2.class);
}

SecretContentsAtVersionResponseV2 contentsAtVersion(SecretContentsAtVersionRequestV2 request) throws IOException {
RequestBody body = RequestBody.create(JSON, mapper.writeValueAsString(request));
Request get = clientRequest("/automation/v2/secrets/request/contents-at-version").post(body).build();
Response httpResponse = mutualSslClient.newCall(get).execute();
assertThat(httpResponse.code()).isEqualTo(200);
return mapper.readValue(httpResponse.body().byteStream(), SecretContentsAtVersionResponseV2.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();
Expand Down

0 comments on commit 840b264

Please sign in to comment.