fix(acm): keep the Export option on RequestCertificate

RequestCertificate always stored Export as DISABLED, so an exportable public
certificate could never be exported. Read Export and the transparency logging
preference the way UpdateCertificateOptions does, rejecting values other than
ENABLED and DISABLED.
This commit is contained in:
Zakaria
2026-10-25 12:30:11 -04:00
parent eaf5ca6ade
commit d8d7dd021b
2 changed files with 74 additions and 2 deletions
@@ -563,8 +563,10 @@ public class AcmJsonHandler {
private CertificateOptions parseOptions(JsonNode optionsNode) { private CertificateOptions parseOptions(JsonNode optionsNode) {
if (optionsNode.isMissingNode()) return null; if (optionsNode.isMissingNode()) return null;
String ctPref = optionsNode.path("CertificateTransparencyLoggingPreference").asText("ENABLED"); String ctPref = parseOptionValue(optionsNode, "CertificateTransparencyLoggingPreference");
return new CertificateOptions(ctPref, "DISABLED"); 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<CertificateStatus> parseCertificateStatuses(JsonNode node) { private List<CertificateStatus> parseCertificateStatuses(JsonNode node) {
@@ -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();
}
}