diff --git a/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java b/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java index 8008ef7..43ebd05 100644 --- a/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java +++ b/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java @@ -563,8 +563,10 @@ public class AcmJsonHandler { private CertificateOptions parseOptions(JsonNode optionsNode) { if (optionsNode.isMissingNode()) return null; - String ctPref = optionsNode.path("CertificateTransparencyLoggingPreference").asText("ENABLED"); - return new CertificateOptions(ctPref, "DISABLED"); + String ctPref = parseOptionValue(optionsNode, "CertificateTransparencyLoggingPreference"); + String export = parseOptionValue(optionsNode, "Export"); + // An exportable public certificate is requested with Export ENABLED, and keeps it for life. + return new CertificateOptions(ctPref != null ? ctPref : "ENABLED", export != null ? export : "DISABLED"); } private List parseCertificateStatuses(JsonNode node) { diff --git a/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java b/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java new file mode 100644 index 0000000..b4b13d1 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java @@ -0,0 +1,70 @@ +package io.github.hectorvent.floci.services.acm; + +import io.github.hectorvent.floci.testing.RestAssuredJsonUtils; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +@QuarkusTest +class AcmExportableRequestTest { + + private static final String ACM_CONTENT_TYPE = "application/x-amz-json-1.1"; + + @BeforeAll + static void configureRestAssured() { + RestAssuredJsonUtils.configureAwsContentTypes(); + } + + @Test + void requestCertificateKeepsTheExportOption() { + String exportable = request("{\"DomainName\":\"exportable.example.com\",\"Options\":{\"Export\":\"ENABLED\"}}"); + + call("DescribeCertificate", "{\"CertificateArn\":\"" + exportable + "\"}") + .statusCode(200) + .body("Certificate.Options.Export", equalTo("ENABLED")) + .body("Certificate.Options.CertificateTransparencyLoggingPreference", equalTo("ENABLED")); + + call("ExportCertificate", "{\"CertificateArn\":\"" + exportable + "\",\"Passphrase\":\"c2VjcmV0\"}") + .statusCode(200) + .body("PrivateKey", notNullValue()); + } + + @Test + void requestCertificateWithoutExportIsNotExportable() { + String plain = request("{\"DomainName\":\"plain.example.com\",\"Options\":{\"CertificateTransparencyLoggingPreference\":\"DISABLED\"}}"); + + call("DescribeCertificate", "{\"CertificateArn\":\"" + plain + "\"}") + .statusCode(200) + .body("Certificate.Options.Export", equalTo("DISABLED")) + .body("Certificate.Options.CertificateTransparencyLoggingPreference", equalTo("DISABLED")); + + call("ExportCertificate", "{\"CertificateArn\":\"" + plain + "\",\"Passphrase\":\"c2VjcmV0\"}") + .statusCode(400) + .body("__type", equalTo("ValidationException")); + } + + @Test + void requestCertificateRejectsAnUnknownExportValue() { + call("RequestCertificate", "{\"DomainName\":\"bad.example.com\",\"Options\":{\"Export\":\"MAYBE\"}}") + .statusCode(400) + .body("__type", equalTo("ValidationException")); + } + + private String request(String body) { + return call("RequestCertificate", body).statusCode(200).extract().jsonPath().getString("CertificateArn"); + } + + private io.restassured.response.ValidatableResponse call(String action, String body) { + return given() + .header("X-Amz-Target", "CertificateManager." + action) + .contentType(ACM_CONTENT_TYPE) + .body(body) + .when() + .post("/") + .then(); + } +}