diff --git a/.changelog/10213.txt b/.changelog/10213.txt new file mode 100644 index 00000000000..350bfe6d3a8 --- /dev/null +++ b/.changelog/10213.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_acmpca_certificate +``` + +```release-note:new-data-source +aws_acmpca_certificate +``` diff --git a/.changelog/17850.txt b/.changelog/17850.txt new file mode 100644 index 00000000000..ccda8dc4aa2 --- /dev/null +++ b/.changelog/17850.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_acmpca_certificate_authority_certificate +``` diff --git a/aws/data_source_aws_acmpca_certificate.go b/aws/data_source_aws_acmpca_certificate.go new file mode 100644 index 00000000000..6ac3d044f1a --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate.go @@ -0,0 +1,60 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAwsAcmpcaCertificate() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAcmpcaCertificateRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsAcmpcaCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + certificateArn := d.Get("arn").(string) + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(certificateArn), + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + log.Printf("[DEBUG] Reading ACM PCA Certificate: %s", getCertificateInput) + + certificateOutput, err := conn.GetCertificate(getCertificateInput) + if err != nil { + return fmt.Errorf("error reading ACM PCA Certificate (%s): %w", certificateArn, err) + } + + d.SetId(certificateArn) + d.Set("certificate", aws.StringValue(certificateOutput.Certificate)) + d.Set("certificate_chain", aws.StringValue(certificateOutput.CertificateChain)) + + return nil +} diff --git a/aws/data_source_aws_acmpca_certificate_authority.go b/aws/data_source_aws_acmpca_certificate_authority.go index 1047cd971ef..aaf324a18dc 100644 --- a/aws/data_source_aws_acmpca_certificate_authority.go +++ b/aws/data_source_aws_acmpca_certificate_authority.go @@ -103,15 +103,15 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority: %s", describeCertificateAuthorityInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority: %s", describeCertificateAuthorityInput) describeCertificateAuthorityOutput, err := conn.DescribeCertificateAuthority(describeCertificateAuthorityInput) if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority: %w", err) } if describeCertificateAuthorityOutput.CertificateAuthority == nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: not found") + return fmt.Errorf("error reading ACM PCA Certificate Authority: not found") } certificateAuthority := describeCertificateAuthorityOutput.CertificateAuthority @@ -131,14 +131,14 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) if err != nil { // Returned when in PENDING_CERTIFICATE status // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. if !tfawserr.ErrCodeEquals(err, acmpca.ErrCodeInvalidStateException) { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate: %w", err) } } @@ -153,11 +153,11 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in CertificateAuthorityArn: aws.String(certificateAuthorityArn), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %w", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate Signing Request: %w", err) } d.Set("certificate_signing_request", "") @@ -168,7 +168,7 @@ func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta in tags, err := keyvaluetags.AcmpcaListTags(conn, certificateAuthorityArn) if err != nil { - return fmt.Errorf("error listing tags for ACMPCA Certificate Authority (%s): %w", certificateAuthorityArn, err) + return fmt.Errorf("error listing tags for ACM PCA Certificate Authority (%s): %w", certificateAuthorityArn, err) } if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { diff --git a/aws/data_source_aws_acmpca_certificate_test.go b/aws/data_source_aws_acmpca_certificate_test.go new file mode 100644 index 00000000000..e910995ddf3 --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate_test.go @@ -0,0 +1,87 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceAwsAcmpcaCertificate_Basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + dataSourceName := "data.aws_acmpca_certificate.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsAcmpcaCertificateConfig_NonExistent, + ExpectError: regexp.MustCompile(`ResourceNotFoundException`), + }, + { + Config: testAccDataSourceAwsAcmpcaCertificateConfig_ARN(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate", resourceName, "certificate"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate_chain", resourceName, "certificate_chain"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate_authority_arn", resourceName, "certificate_authority_arn"), + ), + }, + }, + }) +} + +func testAccDataSourceAwsAcmpcaCertificateConfig_ARN(rName string) string { + return fmt.Sprintf(` +data "aws_acmpca_certificate" "test" { + arn = aws_acmpca_certificate.test.arn + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +data "aws_partition" "current" {} +`, rName) +} + +const testAccDataSourceAwsAcmpcaCertificateConfig_NonExistent = ` +data "aws_acmpca_certificate" "test" { + arn = "arn:${data.aws_partition.current.partition}:acm-pca:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:certificate-authority/does-not-exist/certificate/does-not-exist" + certificate_authority_arn = "arn:${data.aws_partition.current.partition}:acm-pca:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:certificate-authority/does-not-exist" +} + +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "current" {} +` diff --git a/aws/internal/service/acmpca/finder/finder.go b/aws/internal/service/acmpca/finder/finder.go index f6dfc51bb5b..7cf9c2f36a8 100644 --- a/aws/internal/service/acmpca/finder/finder.go +++ b/aws/internal/service/acmpca/finder/finder.go @@ -3,6 +3,8 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) // CertificateAuthorityByARN returns the certificate authority corresponding to the specified ARN. @@ -23,3 +25,31 @@ func CertificateAuthorityByARN(conn *acmpca.ACMPCA, arn string) (*acmpca.Certifi return output.CertificateAuthority, nil } + +// CertificateAuthorityCertificateByARN returns the certificate for the certificate authority corresponding to the specified ARN. +// Returns a resource.NotFoundError if no certificate authority is found or the certificate authority does not have a certificate assigned. +func CertificateAuthorityCertificateByARN(conn *acmpca.ACMPCA, arn string) (*acmpca.GetCertificateAuthorityCertificateOutput, error) { + input := &acmpca.GetCertificateAuthorityCertificateInput{ + CertificateAuthorityArn: aws.String(arn), + } + + output, err := conn.GetCertificateAuthorityCertificate(input) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + Message: "empty result", + LastRequest: input, + } + } + + return output, nil +} diff --git a/aws/internal/service/acmpca/waiter/waiter.go b/aws/internal/service/acmpca/waiter/waiter.go index c9c4c803f9d..0205dabec2a 100644 --- a/aws/internal/service/acmpca/waiter/waiter.go +++ b/aws/internal/service/acmpca/waiter/waiter.go @@ -24,3 +24,7 @@ func CertificateAuthorityCreated(conn *acmpca.ACMPCA, arn string, timeout time.D return nil, err } + +const ( + CertificateAuthorityActiveTimeout = 1 * time.Minute +) diff --git a/aws/provider.go b/aws/provider.go index 5de81c4636c..a8078c1ff52 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -169,6 +169,7 @@ func Provider() *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ "aws_acm_certificate": dataSourceAwsAcmCertificate(), "aws_acmpca_certificate_authority": dataSourceAwsAcmpcaCertificateAuthority(), + "aws_acmpca_certificate": dataSourceAwsAcmpcaCertificate(), "aws_ami": dataSourceAwsAmi(), "aws_ami_ids": dataSourceAwsAmiIds(), "aws_api_gateway_api_key": dataSourceAwsApiGatewayApiKey(), @@ -412,6 +413,8 @@ func Provider() *schema.Provider { "aws_acm_certificate": resourceAwsAcmCertificate(), "aws_acm_certificate_validation": resourceAwsAcmCertificateValidation(), "aws_acmpca_certificate_authority": resourceAwsAcmpcaCertificateAuthority(), + "aws_acmpca_certificate_authority_certificate": resourceAwsAcmpcaCertificateAuthorityCertificate(), + "aws_acmpca_certificate": resourceAwsAcmpcaCertificate(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), diff --git a/aws/resource_aws_acmpca_certificate.go b/aws/resource_aws_acmpca_certificate.go new file mode 100644 index 00000000000..5a6317d0ec4 --- /dev/null +++ b/aws/resource_aws_acmpca_certificate.go @@ -0,0 +1,281 @@ +package aws + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "regexp" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/waiter" +) + +func resourceAwsAcmpcaCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmpcaCertificateCreate, + Read: resourceAwsAcmpcaCertificateRead, + Delete: resourceAwsAcmpcaCertificateRevoke, + + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + re := regexp.MustCompile(`arn:.+:certificate-authority/[^/]+`) + authorityArn := re.FindString(d.Id()) + if authorityArn == "" { + return nil, fmt.Errorf("Unexpected format for ID (%q), expected ACM PCA Certificate ARN", d.Id()) + } + + d.Set("certificate_authority_arn", authorityArn) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "certificate_signing_request": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "signing_algorithm": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(acmpca.SigningAlgorithm_Values(), false), + }, + "validity": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(acmpca.ValidityPeriodType_Values(), false), + }, + "value": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, + }, + }, + }, + }, + "template_arn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAcmPcaTemplateArn, + }, + }, + } +} + +func resourceAwsAcmpcaCertificateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + certificateAuthorityArn := d.Get("certificate_authority_arn").(string) + input := &acmpca.IssueCertificateInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + Csr: []byte(d.Get("certificate_signing_request").(string)), + IdempotencyToken: aws.String(resource.UniqueId()), + SigningAlgorithm: aws.String(d.Get("signing_algorithm").(string)), + } + validity, err := expandAcmpcaValidity(d.Get("validity").([]interface{})) + if err != nil { + return fmt.Errorf("error issuing ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + input.Validity = validity + + if v, ok := d.Get("template_arn").(string); ok && v != "" { + input.TemplateArn = aws.String(v) + } + + var output *acmpca.IssueCertificateOutput + err = resource.Retry(waiter.CertificateAuthorityActiveTimeout, func() *resource.RetryError { + var err error + output, err = conn.IssueCertificate(input) + if tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "The certificate authority is not in a valid state for issuing certificates") { + return resource.RetryableError(err) + } + if err != nil { + return resource.NonRetryableError(err) + } + return nil + }) + if isResourceTimeoutError(err) { + output, err = conn.IssueCertificate(input) + } + + if err != nil { + return fmt.Errorf("error issuing ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + + d.SetId(aws.StringValue(output.CertificateArn)) + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: output.CertificateArn, + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + err = conn.WaitUntilCertificateIssued(getCertificateInput) + if err != nil { + return fmt.Errorf("error waiting for ACM PCA Certificate Authority (%s) to issue Certificate (%s), error: %w", certificateAuthorityArn, d.Id(), err) + } + + return resourceAwsAcmpcaCertificateRead(d, meta) +} + +func resourceAwsAcmpcaCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + getCertificateInput := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(d.Id()), + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + } + + log.Printf("[DEBUG] Reading ACM PCA Certificate: %s", getCertificateInput) + + certificateOutput, err := conn.GetCertificate(getCertificateInput) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] ACM PCA Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading ACM PCA Certificate: %s", err) + } + + d.Set("arn", d.Id()) + d.Set("certificate", aws.StringValue(certificateOutput.Certificate)) + d.Set("certificate_chain", aws.StringValue(certificateOutput.CertificateChain)) + + return nil +} + +func resourceAwsAcmpcaCertificateRevoke(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + block, _ := pem.Decode([]byte(d.Get("certificate").(string))) + if block == nil { + log.Printf("[WARN] Failed to parse ACM PCA Certificate (%s)", d.Id()) + return nil + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("Failed to parse ACM PCA Certificate (%s): %w", d.Id(), err) + } + + input := &acmpca.RevokeCertificateInput{ + CertificateAuthorityArn: aws.String(d.Get("certificate_authority_arn").(string)), + CertificateSerial: aws.String(fmt.Sprintf("%x", cert.SerialNumber)), + RevocationReason: aws.String(acmpca.RevocationReasonUnspecified), + } + _, err = conn.RevokeCertificate(input) + + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) || + tfawserr.ErrCodeEquals(err, acmpca.ErrCodeRequestAlreadyProcessedException) || + tfawserr.ErrCodeEquals(err, acmpca.ErrCodeRequestInProgressException) || + tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidRequestException, "Self-signed certificate can not be revoked") { + return nil + } + if err != nil { + return fmt.Errorf("error revoking ACM PCA Certificate (%s): %w", d.Id(), err) + } + + return nil +} + +func validateAcmPcaTemplateArn(v interface{}, k string) (ws []string, errors []error) { + wsARN, errorsARN := validateArn(v, k) + ws = append(ws, wsARN...) + errors = append(errors, errorsARN...) + + if len(errors) == 0 { + value := v.(string) + parsedARN, _ := arn.Parse(value) + + if parsedARN.Service != acmpca.ServiceName { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: service must be \""+acmpca.ServiceName+"\", was %q)", k, value, parsedARN.Service)) + } + + if parsedARN.Region != "" { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: region must be empty, was %q)", k, value, parsedARN.Region)) + } + + if parsedARN.AccountID != "" { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: account ID must be empty, was %q)", k, value, parsedARN.AccountID)) + } + + if !strings.HasPrefix(parsedARN.Resource, "template/") { + errors = append(errors, fmt.Errorf("%q (%s) is not a valid ACM PCA template ARN: expected resource to start with \"template/\", was %q)", k, value, parsedARN.Resource)) + } + } + + return ws, errors +} + +func expandAcmpcaValidity(l []interface{}) (*acmpca.Validity, error) { + if len(l) == 0 { + return nil, nil + } + + m := l[0].(map[string]interface{}) + + valueType := m["type"].(string) + result := &acmpca.Validity{ + Type: aws.String(valueType), + } + + i, err := expandAcmpcaValidityValue(valueType, m["value"].(string)) + if err != nil { + return nil, fmt.Errorf("error parsing value %q: %w", m["value"].(string), err) + } + result.Value = aws.Int64(i) + + return result, nil +} + +func expandAcmpcaValidityValue(valueType, v string) (int64, error) { + if valueType == acmpca.ValidityPeriodTypeEndDate { + date, err := time.Parse(time.RFC3339, v) + if err != nil { + return 0, err + } + v = date.UTC().Format("20060102150405") // YYYYMMDDHHMMSS + } + + return strconv.ParseInt(v, 10, 64) +} diff --git a/aws/resource_aws_acmpca_certificate_authority.go b/aws/resource_aws_acmpca_certificate_authority.go index a2ea02cfae9..2455f26e22a 100644 --- a/aws/resource_aws_acmpca_certificate_authority.go +++ b/aws/resource_aws_acmpca_certificate_authority.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -285,7 +286,7 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in input.Tags = tags } - log.Printf("[DEBUG] Creating ACMPCA Certificate Authority: %s", input) + log.Printf("[DEBUG] Creating ACM PCA Certificate Authority: %s", input) var output *acmpca.CreateCertificateAuthorityOutput err := resource.Retry(1*time.Minute, func() *resource.RetryError { var err error @@ -303,7 +304,7 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in output, err = conn.CreateCertificateAuthority(input) } if err != nil { - return fmt.Errorf("error creating ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error creating ACM PCA Certificate Authority: %s", err) } d.SetId(aws.StringValue(output.CertificateAuthorityArn)) @@ -311,7 +312,7 @@ func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta in _, err = waiter.CertificateAuthorityCreated(conn, d.Id(), d.Timeout(schema.TimeoutCreate)) if err != nil { - return fmt.Errorf("error waiting for ACMPCA Certificate Authority %q to be active or pending certificate: %s", d.Id(), err) + return fmt.Errorf("error waiting for ACM PCA Certificate Authority %q to be active or pending certificate: %s", d.Id(), err) } return resourceAwsAcmpcaCertificateAuthorityRead(d, meta) @@ -324,17 +325,17 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte certificateAuthority, err := finder.CertificateAuthorityByARN(conn, d.Id()) if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority: %s", err) } if certificateAuthority == nil || aws.StringValue(certificateAuthority.Status) == acmpca.CertificateAuthorityStatusDeleted { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -361,19 +362,19 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte CertificateAuthorityArn: aws.String(d.Id()), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) if err != nil { if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } // Returned when in PENDING_CERTIFICATE status // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %s", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate: %s", err) } } @@ -388,17 +389,17 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte CertificateAuthorityArn: aws.String(d.Id()), } - log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + log.Printf("[DEBUG] Reading ACM PCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) if err != nil { if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + log.Printf("[WARN] ACM PCA Certificate Authority (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { - return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %s", err) + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate Signing Request: %s", err) } } @@ -410,7 +411,7 @@ func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta inte tags, err := keyvaluetags.AcmpcaListTags(conn, d.Id()) if err != nil { - return fmt.Errorf("error listing tags for ACMPCA Certificate Authority (%s): %s", d.Id(), err) + return fmt.Errorf("error listing tags for ACM PCA Certificate Authority (%s): %s", d.Id(), err) } if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { @@ -442,10 +443,10 @@ func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta in } if updateCertificateAuthority { - log.Printf("[DEBUG] Updating ACMPCA Certificate Authority: %s", input) + log.Printf("[DEBUG] Updating ACM PCA Certificate Authority: %s", input) _, err := conn.UpdateCertificateAuthority(input) if err != nil { - return fmt.Errorf("error updating ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error updating ACM PCA Certificate Authority: %s", err) } } @@ -453,7 +454,7 @@ func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta in o, n := d.GetChange("tags") if err := keyvaluetags.AcmpcaUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating ACMPCA Certificate Authority (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating ACM PCA Certificate Authority (%s) tags: %s", d.Id(), err) } } @@ -463,21 +464,34 @@ func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta in func resourceAwsAcmpcaCertificateAuthorityDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).acmpcaconn - input := &acmpca.DeleteCertificateAuthorityInput{ + // The Certificate Authority must be in PENDING_CERTIFICATE or DISABLED state before deleting. + updateInput := &acmpca.UpdateCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + Status: aws.String(acmpca.CertificateAuthorityStatusDisabled), + } + _, err := conn.UpdateCertificateAuthority(updateInput) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } + if err != nil && !tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "The certificate authority must be in the ACTIVE or DISABLED state to be updated") { + return fmt.Errorf("error setting ACM PCA Certificate Authority (%s) to DISABLED status before deleting: %w", d.Id(), err) + } + + deleteInput := &acmpca.DeleteCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(d.Id()), } if v, exists := d.GetOk("permanent_deletion_time_in_days"); exists { - input.PermanentDeletionTimeInDays = aws.Int64(int64(v.(int))) + deleteInput.PermanentDeletionTimeInDays = aws.Int64(int64(v.(int))) } - log.Printf("[DEBUG] Deleting ACMPCA Certificate Authority: %s", input) - _, err := conn.DeleteCertificateAuthority(input) + log.Printf("[DEBUG] Deleting ACM PCA Certificate Authority: %s", deleteInput) + _, err = conn.DeleteCertificateAuthority(deleteInput) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } if err != nil { - if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { - return nil - } - return fmt.Errorf("error deleting ACMPCA Certificate Authority: %s", err) + return fmt.Errorf("error deleting ACM PCA Certificate Authority (%s): %w", d.Id(), err) } return nil diff --git a/aws/resource_aws_acmpca_certificate_authority_certificate.go b/aws/resource_aws_acmpca_certificate_authority_certificate.go new file mode 100644 index 00000000000..9309f8cf6ec --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority_certificate.go @@ -0,0 +1,89 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAcmpcaCertificateAuthorityCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmpcaCertificateAuthorityCertificateCreate, + Read: resourceAwsAcmpcaCertificateAuthorityCertificateRead, + Delete: schema.Noop, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "certificate": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 32768), + }, + "certificate_authority_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "certificate_chain": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 2097152), + }, + }, + } +} + +func resourceAwsAcmpcaCertificateAuthorityCertificateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + certificateAuthorityArn := d.Get("certificate_authority_arn").(string) + + input := &acmpca.ImportCertificateAuthorityCertificateInput{ + Certificate: []byte(d.Get("certificate").(string)), + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + if v, ok := d.Get("certificate_chain").(string); ok && v != "" { + input.CertificateChain = []byte(v) + } + + _, err := conn.ImportCertificateAuthorityCertificate(input) + if err != nil { + return fmt.Errorf("error associating ACM PCA Certificate with Certificate Authority (%s): %w", certificateAuthorityArn, err) + } + + d.SetId(certificateAuthorityArn) + + return resourceAwsAcmpcaCertificateAuthorityCertificateRead(d, meta) +} + +func resourceAwsAcmpcaCertificateAuthorityCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + output, err := finder.CertificateAuthorityCertificateByARN(conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ACM PCA Certificate Authority Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("error reading ACM PCA Certificate Authority Certificate (%s): %w", d.Id(), err) + } + + d.Set("certificate_authority_arn", d.Id()) + d.Set("certificate", aws.StringValue(output.Certificate)) + d.Set("certificate_chain", aws.StringValue(output.CertificateChain)) + + return nil +} diff --git a/aws/resource_aws_acmpca_certificate_authority_certificate_test.go b/aws/resource_aws_acmpca_certificate_authority_certificate_test.go new file mode 100644 index 00000000000..bd4231c4506 --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority_certificate_test.go @@ -0,0 +1,279 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/acmpca/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority_certificate.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority_certificate.test" + updatedResourceName := "aws_acmpca_certificate_authority_certificate.updated" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(updatedResourceName, &v), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate", "aws_acmpca_certificate.updated", "certificate"), + resource.TestCheckResourceAttrPair(updatedResourceName, "certificate_chain", "aws_acmpca_certificate.updated", "certificate_chain"), + ), + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(t *testing.T) { + var v acmpca.GetCertificateAuthorityCertificateOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority_certificate.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: nil, // Certificate authority certificates cannot be deleted + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", "aws_acmpca_certificate_authority.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate", "aws_acmpca_certificate.test", "certificate"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_chain", "aws_acmpca_certificate.test", "certificate_chain"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAcmpcaCertificateAuthorityCertificateExists(resourceName string, certificate *acmpca.GetCertificateAuthorityCertificateOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + + output, err := finder.CertificateAuthorityCertificateByARN(conn, rs.Primary.ID) + if err != nil { + return err + } + if tfresource.NotFound(err) { + return fmt.Errorf("ACM PCA Certificate (%s) does not exist", rs.Primary.ID) + } + + *certificate = *output + + return nil + } +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_RootCA(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +data "aws_partition" "current" {} +`, rName) +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_UpdateRootCA(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "updated" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.updated.certificate + certificate_chain = aws_acmpca_certificate.updated.certificate_chain +} + +resource "aws_acmpca_certificate" "updated" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +data "aws_partition" "current" {} +`, rName) +} + +func testAccAwsAcmpcaCertificateAuthorityCertificate_SubordinateCA(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "SUBORDINATE" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "sub.%[1]s.com" + } + } +} + +resource "aws_acmpca_certificate_authority" "root" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +resource "aws_acmpca_certificate_authority_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + + certificate = aws_acmpca_certificate.root.certificate + certificate_chain = aws_acmpca_certificate.root.certificate_chain +} + +resource "aws_acmpca_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.root.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 2 + } +} + +data "aws_partition" "current" {} +`, rName) +} diff --git a/aws/resource_aws_acmpca_certificate_authority_migrate.go b/aws/resource_aws_acmpca_certificate_authority_migrate.go index 1e039f8975b..156ade901ff 100644 --- a/aws/resource_aws_acmpca_certificate_authority_migrate.go +++ b/aws/resource_aws_acmpca_certificate_authority_migrate.go @@ -10,7 +10,7 @@ import ( func resourceAwsAcmpcaCertificateAuthorityMigrateState(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { switch v { case 0: - log.Println("[INFO] Found ACMPCA Certificate Authority state v0; migrating to v1") + log.Println("[INFO] Found ACM PCA Certificate Authority state v0; migrating to v1") return migrateAcmpcaCertificateAuthorityStateV0toV1(is) default: return is, fmt.Errorf("Unexpected schema version: %d", v) @@ -19,7 +19,7 @@ func resourceAwsAcmpcaCertificateAuthorityMigrateState(v int, is *terraform.Inst func migrateAcmpcaCertificateAuthorityStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { if is.Empty() || is.Attributes == nil { - log.Println("[DEBUG] Empty ACMPCA Certificate Authority state; nothing to migrate.") + log.Println("[DEBUG] Empty ACM PCA Certificate Authority state; nothing to migrate.") return is, nil } diff --git a/aws/resource_aws_acmpca_certificate_authority_test.go b/aws/resource_aws_acmpca_certificate_authority_test.go index 915d38c260b..031d2a974ee 100644 --- a/aws/resource_aws_acmpca_certificate_authority_test.go +++ b/aws/resource_aws_acmpca_certificate_authority_test.go @@ -31,13 +31,13 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { certificateAuthorities, err := listAcmpcaCertificateAuthorities(conn) if err != nil { if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping ACMPCA Certificate Authorities sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping ACM PCA Certificate Authorities sweep for %s: %s", region, err) return nil } - return fmt.Errorf("Error retrieving ACMPCA Certificate Authorities: %w", err) + return fmt.Errorf("Error retrieving ACM PCA Certificate Authorities: %w", err) } if len(certificateAuthorities) == 0 { - log.Print("[DEBUG] No ACMPCA Certificate Authorities to sweep") + log.Print("[DEBUG] No ACM PCA Certificate Authorities to sweep") return nil } @@ -47,7 +47,7 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { arn := aws.StringValue(certificateAuthority.Arn) if aws.StringValue(certificateAuthority.Status) == acmpca.CertificateAuthorityStatusActive { - log.Printf("[INFO] Disabling ACMPCA Certificate Authority: %s", arn) + log.Printf("[INFO] Disabling ACM PCA Certificate Authority: %s", arn) _, err := conn.UpdateCertificateAuthority(&acmpca.UpdateCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(arn), Status: aws.String(acmpca.CertificateAuthorityStatusDisabled), @@ -56,14 +56,14 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { continue } if err != nil { - sweeperErr := fmt.Errorf("error disabling ACMPCA Certificate Authority (%s): %w", arn, err) + sweeperErr := fmt.Errorf("error disabling ACM PCA Certificate Authority (%s): %w", arn, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue } } - log.Printf("[INFO] Deleting ACMPCA Certificate Authority: %s", arn) + log.Printf("[INFO] Deleting ACM PCA Certificate Authority: %s", arn) _, err := conn.DeleteCertificateAuthority(&acmpca.DeleteCertificateAuthorityInput{ CertificateAuthorityArn: aws.String(arn), PermanentDeletionTimeInDays: aws.Int64(int64(7)), @@ -72,7 +72,7 @@ func testSweepAcmpcaCertificateAuthorities(region string) error { continue } if err != nil { - sweeperErr := fmt.Errorf("error deleting ACMPCA Certificate Authority (%s): %w", arn, err) + sweeperErr := fmt.Errorf("error deleting ACM PCA Certificate Authority (%s): %w", arn, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue @@ -196,6 +196,31 @@ func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { }) } +func TestAccAwsAcmpcaCertificateAuthority_DeleteFromActiveState(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_WithRootCertificate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "type", acmpca.CertificateAuthorityTypeRoot), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + // Since the status of the CA is changed by importing the certificate in + // aws_acmpca_certificate_authority_certificate, the value of `status` is no longer accurate + // resource.TestCheckResourceAttr(resourceName, "status", acmpca.CertificateAuthorityStatusActive), + ), + }, + }, + }) +} + func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_CustomCname(t *testing.T) { var certificateAuthority acmpca.CertificateAuthority rName := acctest.RandomWithPrefix("tf-acc-test") @@ -484,12 +509,11 @@ func testAccCheckAwsAcmpcaCertificateAuthorityDestroy(s *terraform.State) error } if output != nil && output.CertificateAuthority != nil && aws.StringValue(output.CertificateAuthority.Arn) == rs.Primary.ID && aws.StringValue(output.CertificateAuthority.Status) != acmpca.CertificateAuthorityStatusDeleted { - return fmt.Errorf("ACMPCA Certificate Authority %q still exists in non-DELETED state: %s", rs.Primary.ID, aws.StringValue(output.CertificateAuthority.Status)) + return fmt.Errorf("ACM PCA Certificate Authority %q still exists in non-DELETED state: %s", rs.Primary.ID, aws.StringValue(output.CertificateAuthority.Status)) } } return nil - } func testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName string, certificateAuthority *acmpca.CertificateAuthority) resource.TestCheckFunc { @@ -511,7 +535,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName string, certif } if output == nil || output.CertificateAuthority == nil { - return fmt.Errorf("ACMPCA Certificate Authority %q does not exist", rs.Primary.ID) + return fmt.Errorf("ACM PCA Certificate Authority %q does not exist", rs.Primary.ID) } *certificateAuthority = *output.CertificateAuthority @@ -530,7 +554,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateAuthorityArn: aws.String(arn), }) if err != nil { - return fmt.Errorf("error getting ACMPCA Certificate Authority (%s) CSR: %s", arn, err) + return fmt.Errorf("error getting ACM PCA Certificate Authority (%s) CSR: %s", arn, err) } issueCertResp, err := conn.IssueCertificate(&acmpca.IssueCertificateInput{ @@ -545,7 +569,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a }, }) if err != nil { - return fmt.Errorf("error issuing ACMPCA Certificate Authority (%s) Root CA certificate from CSR: %s", arn, err) + return fmt.Errorf("error issuing ACM PCA Certificate Authority (%s) Root CA certificate from CSR: %s", arn, err) } // Wait for certificate status to become ISSUED. @@ -554,7 +578,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateArn: issueCertResp.CertificateArn, }) if err != nil { - return fmt.Errorf("error waiting for ACMPCA Certificate Authority (%s) Root CA certificate to become ISSUED: %s", arn, err) + return fmt.Errorf("error waiting for ACM PCA Certificate Authority (%s) Root CA certificate to become ISSUED: %s", arn, err) } getCertResp, err := conn.GetCertificate(&acmpca.GetCertificateInput{ @@ -562,7 +586,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a CertificateArn: issueCertResp.CertificateArn, }) if err != nil { - return fmt.Errorf("error getting ACMPCA Certificate Authority (%s) issued Root CA certificate: %s", arn, err) + return fmt.Errorf("error getting ACM PCA Certificate Authority (%s) issued Root CA certificate: %s", arn, err) } _, err = conn.ImportCertificateAuthorityCertificate(&acmpca.ImportCertificateAuthorityCertificateInput{ @@ -570,7 +594,7 @@ func testAccCheckAwsAcmpcaCertificateAuthorityActivateCA(certificateAuthority *a Certificate: []byte(aws.StringValue(getCertResp.Certificate)), }) if err != nil { - return fmt.Errorf("error importing ACMPCA Certificate Authority (%s) Root CA certificate: %s", arn, err) + return fmt.Errorf("error importing ACM PCA Certificate Authority (%s) Root CA certificate: %s", arn, err) } return err @@ -628,6 +652,46 @@ resource "aws_acmpca_certificate_authority" "test" { `, enabled, certificateAuthorityType, rName) } +func testAccAwsAcmpcaCertificateAuthorityConfig_WithRootCertificate(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +data "aws_partition" "current" {} +`, rName) +} + const testAccAwsAcmpcaCertificateAuthorityConfig_Required = ` resource "aws_acmpca_certificate_authority" "test" { certificate_authority_configuration { diff --git a/aws/resource_aws_acmpca_certificate_test.go b/aws/resource_aws_acmpca_certificate_test.go new file mode 100644 index 00000000000..516502483cc --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_test.go @@ -0,0 +1,505 @@ +package aws + +import ( + "fmt" + "regexp" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAwsAcmpcaCertificate_RootCertificate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + certificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_RootCertificate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttr(resourceName, "certificate_chain", ""), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", certificateAuthorityResourceName, "arn"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_signing_request"), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "YEARS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA512WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/RootCACertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_SubordinateCertificate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + rootCertificateAuthorityResourceName := "aws_acmpca_certificate_authority.root" + subordinateCertificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_SubordinateCertificate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", rootCertificateAuthorityResourceName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_signing_request", subordinateCertificateAuthorityResourceName, "certificate_signing_request"), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "YEARS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA512WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/SubordinateCACertificate_PathLen0/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_EndEntityCertificate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + csr, _ := tlsRsaX509CertificateRequestPem(4096, "terraformtest1.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_EndEntityCertificate(rName, tlsPemEscapeNewlines(csr)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", "1"), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "DAYS"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_Validity_EndDate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + csr, _ := tlsRsaX509CertificateRequestPem(4096, "terraformtest1.com") + later := time.Now().Add(time.Minute * 10).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_Validity_EndDate(rName, tlsPemEscapeNewlines(csr), later), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", later), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "END_DATE"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificate_Validity_Absolute(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate.test" + csr, _ := tlsRsaX509CertificateRequestPem(4096, "terraformtest1.com") + later := time.Now().Add(time.Minute * 10).Unix() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateConfig_Validity_Absolute(rName, tlsPemEscapeNewlines(csr), later), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsAcmpcaCertificateExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm-pca", regexp.MustCompile(`certificate-authority/.+/certificate/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, "certificate"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_chain"), + resource.TestCheckResourceAttr(resourceName, "certificate_signing_request", csr), + resource.TestCheckResourceAttr(resourceName, "validity.0.value", strconv.FormatInt(later, 10)), + resource.TestCheckResourceAttr(resourceName, "validity.0.type", "ABSOLUTE"), + resource.TestCheckResourceAttr(resourceName, "signing_algorithm", "SHA256WITHRSA"), + testAccCheckResourceAttrGlobalARNNoAccount(resourceName, "template_arn", "acm-pca", "template/EndEntityCertificate/V1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "certificate_signing_request", + "signing_algorithm", + "template_arn", + "validity", + }, + }, + }, + }) +} + +func testAccCheckAwsAcmpcaCertificateDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_acmpca_certificate" { + continue + } + + input := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(rs.Primary.ID), + CertificateAuthorityArn: aws.String(rs.Primary.Attributes["certificate_authority_arn"]), + } + + output, err := conn.GetCertificate(input) + if tfawserr.ErrCodeEquals(err, acmpca.ErrCodeResourceNotFoundException) { + return nil + } + if tfawserr.ErrMessageContains(err, acmpca.ErrCodeInvalidStateException, "not in the correct state to have issued certificates") { + // This is returned when checking root certificates and the certificate has not been associated with the certificate authority + return nil + } + if err != nil { + return err + } + + if output != nil { + return fmt.Errorf("ACM PCA Certificate (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsAcmpcaCertificateExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + input := &acmpca.GetCertificateInput{ + CertificateArn: aws.String(rs.Primary.ID), + CertificateAuthorityArn: aws.String(rs.Primary.Attributes["certificate_authority_arn"]), + } + + output, err := conn.GetCertificate(input) + + if err != nil { + return err + } + + if output == nil || output.Certificate == nil { + return fmt.Errorf("ACM PCA Certificate %q does not exist", rs.Primary.ID) + } + + return nil + } +} + +func testAccAwsAcmpcaCertificateConfig_RootCertificate(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +data "aws_partition" "current" {} +`, rName) +} + +func testAccAwsAcmpcaCertificateConfig_SubordinateCertificate(rName string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(rName), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "test" { + permanent_deletion_time_in_days = 7 + type = "SUBORDINATE" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "sub.%[1]s.com" + } + } +} +`, rName)) +} + +func testAccAwsAcmpcaCertificateConfig_EndEntityCertificate(rName, csr string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(rName), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[2]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "DAYS" + value = 1 + } +} +`, rName, csr)) +} + +func testAccAwsAcmpcaCertificateConfig_Validity_EndDate(rName, csr, expiry string) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(rName), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[2]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "END_DATE" + value = %[3]q + } +} +`, rName, csr, expiry)) +} + +func testAccAwsAcmpcaCertificateConfig_Validity_Absolute(rName, csr string, expiry int64) string { + return composeConfig( + testAccAcmpcaCertificateBaseRootCAConfig(rName), + fmt.Sprintf(` +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = "%[2]s" + signing_algorithm = "SHA256WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/EndEntityCertificate/V1" + + validity { + type = "ABSOLUTE" + value = %[3]d + } +} +`, rName, csr, expiry)) +} + +func testAccAcmpcaCertificateBaseRootCAConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "root" { + permanent_deletion_time_in_days = 7 + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "%[1]s.com" + } + } +} + +resource "aws_acmpca_certificate_authority_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + + certificate = aws_acmpca_certificate.root.certificate + certificate_chain = aws_acmpca_certificate.root.certificate_chain +} + +resource "aws_acmpca_certificate" "root" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.root.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 2 + } +} + +data "aws_partition" "current" {} + `, rName) +} + +func TestValidateAcmPcaTemplateArn(t *testing.T) { + validNames := []string{ + "arn:aws:acm-pca:::template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + "arn:aws-us-gov:acm-pca:::template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws-us-gov:acm-pca:::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + } + for _, v := range validNames { + _, errors := validateAcmPcaTemplateArn(v, "template_arn") + if len(errors) != 0 { + t.Fatalf("%q should be a valid ACM PCA ARN: %q", v, errors) + } + } + + invalidNames := []string{ + "arn", + "arn:aws:s3:::my_corporate_bucket/exampleobject.png", // lintignore:AWSAT005 + "arn:aws:acm-pca:us-west-2::template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT003,AWSAT005 + "arn:aws:acm-pca::123456789012:template/EndEntityCertificate/V1", // lintignore:AWSAT005 + "arn:aws:acm-pca:::not-a-template/SubordinateCACertificate_PathLen0/V1", // lintignore:AWSAT005 + } + for _, v := range invalidNames { + _, errors := validateAcmPcaTemplateArn(v, "template_arn") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid ARN", v) + } + } +} + +func TestExpandAcmpcaValidityValue(t *testing.T) { + testCases := []struct { + Type string + Value string + Expected int64 + }{ + { + Type: acmpca.ValidityPeriodTypeEndDate, + Value: "2021-02-26T16:04:00Z", + Expected: 20210226160400, + }, + { + Type: acmpca.ValidityPeriodTypeEndDate, + Value: "2021-02-26T16:04:00-08:00", + Expected: 20210227000400, + }, + { + Type: acmpca.ValidityPeriodTypeAbsolute, + Value: "1614385420", + Expected: 1614385420, + }, + { + Type: acmpca.ValidityPeriodTypeYears, + Value: "2", + Expected: 2, + }, + } + + for _, testcase := range testCases { + i, _ := expandAcmpcaValidityValue(testcase.Type, testcase.Value) + if i != testcase.Expected { + t.Errorf("%s, %q: expected %d, got %d", testcase.Type, testcase.Value, testcase.Expected, i) + } + } + +} diff --git a/aws/resource_aws_guardduty_filter.go b/aws/resource_aws_guardduty_filter.go index aba50d68e6f..28325100158 100644 --- a/aws/resource_aws_guardduty_filter.go +++ b/aws/resource_aws_guardduty_filter.go @@ -3,7 +3,6 @@ package aws import ( "fmt" "log" - "regexp" "strconv" "strings" "time" @@ -17,11 +16,6 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) -var guardDutyFilterCriterionValidateFunc = validation.Any( - validation.IsRFC3339Time, - validation.StringMatch(regexp.MustCompile(`^\d+$`), "must be an integer value"), -) - func resourceAwsGuardDutyFilter() *schema.Resource { return &schema.Resource{ Create: resourceAwsGuardDutyFilterCreate, @@ -85,22 +79,22 @@ func resourceAwsGuardDutyFilter() *schema.Resource { "greater_than": { Type: schema.TypeString, Optional: true, - ValidateFunc: guardDutyFilterCriterionValidateFunc, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, }, "greater_than_or_equal": { Type: schema.TypeString, Optional: true, - ValidateFunc: guardDutyFilterCriterionValidateFunc, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, }, "less_than": { Type: schema.TypeString, Optional: true, - ValidateFunc: guardDutyFilterCriterionValidateFunc, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, }, "less_than_or_equal": { Type: schema.TypeString, Optional: true, - ValidateFunc: guardDutyFilterCriterionValidateFunc, + ValidateFunc: validateTypeStringIsDateOrPositiveInt, }, }, }, @@ -374,11 +368,7 @@ func expandConditionIntField(field, v string) (int64, error) { return date.UnixNano() / 1000000, nil } - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return 0, err - } - return i, nil + return strconv.ParseInt(v, 10, 64) } func flattenFindingCriteria(findingCriteriaRemote *guardduty.FindingCriteria) []interface{} { diff --git a/aws/tls.go b/aws/tls.go index 8dc0eae06f8..bcf279eb898 100644 --- a/aws/tls.go +++ b/aws/tls.go @@ -13,9 +13,10 @@ import ( ) const ( - pemBlockTypeCertificate = `CERTIFICATE` - pemBlockTypeRsaPrivateKey = `RSA PRIVATE KEY` - pemBlockTypePublicKey = `PUBLIC KEY` + pemBlockTypeCertificate = `CERTIFICATE` + pemBlockTypeRsaPrivateKey = `RSA PRIVATE KEY` + pemBlockTypePublicKey = `PUBLIC KEY` + pemBlockTypeCertificateRequest = `CERTIFICATE REQUEST` ) var tlsX509CertificateSerialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) @@ -240,6 +241,44 @@ func tlsRsaX509SelfSignedCertificatePem(keyPem, commonName string) string { return string(pem.EncodeToMemory(certificateBlock)) } +// tlsRsaX509CertificateRequestPem generates a x509 certificate request PEM string +// and a RSA private key PEM string. +// Wrap with tlsPemEscapeNewlines() to allow simple fmt.Sprintf() +// configurations such as: certificate_signing_request_pem = "%[1]s" private_key_pem = "%[2]s" +func tlsRsaX509CertificateRequestPem(keyBits int, commonName string) (string, string) { + keyBytes, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + //lintignore:R009 + panic(err) + } + + csr := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: commonName, + Organization: []string{"ACME Examples, Inc"}, + }, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, keyBytes) + if err != nil { + //lintignore:R009 + panic(err) + } + + csrBlock := &pem.Block{ + Bytes: csrBytes, + Type: pemBlockTypeCertificateRequest, + } + + keyBlock := &pem.Block{ + Bytes: x509.MarshalPKCS1PrivateKey(keyBytes), + Type: pemBlockTypeRsaPrivateKey, + } + + return string(pem.EncodeToMemory(csrBlock)), string(pem.EncodeToMemory(keyBlock)) +} + func tlsPemEscapeNewlines(pem string) string { return strings.ReplaceAll(pem, "\n", "\\n") } diff --git a/aws/tls_test.go b/aws/tls_test.go index 315907f4db9..d2c9a99d713 100644 --- a/aws/tls_test.go +++ b/aws/tls_test.go @@ -50,3 +50,15 @@ func TestTlsRsaX509SelfSignedCertificatePem(t *testing.T) { t.Errorf("certificate does not contain CERTIFICATE: %s", certificate) } } + +func TestTlsRsaX509CertificateRequestPem(t *testing.T) { + csr, key := tlsRsaX509CertificateRequestPem(2048, "example.com") + + if !strings.Contains(csr, pemBlockTypeCertificateRequest) { + t.Errorf("certificate does not contain CERTIFICATE REQUEST: %s", csr) + } + + if !strings.Contains(key, pemBlockTypeRsaPrivateKey) { + t.Errorf("certificate does not contain RSA PRIVATE KEY: %s", key) + } +} diff --git a/aws/validators.go b/aws/validators.go index 47967a095ec..8820dad3ae6 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -2484,3 +2484,8 @@ func MapKeysDoNotMatch(r *regexp.Regexp, message string) schema.SchemaValidateFu return warnings, errors } } + +var validateTypeStringIsDateOrPositiveInt = validation.Any( + validation.IsRFC3339Time, + validation.StringMatch(regexp.MustCompile(`^\d+$`), "must be a positive integer value"), +) diff --git a/aws/validators_test.go b/aws/validators_test.go index e9941893c64..879e23ee9d3 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -3308,3 +3308,32 @@ func TestValidateUTCTimestamp(t *testing.T) { } } } + +func TestValidateTypeStringIsDateOrInt(t *testing.T) { + validT := []string{ + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05-07:00", + "1234", + "0", + } + + for _, f := range validT { + _, errors := validateTypeStringIsDateOrPositiveInt(f, "parameter") + if len(errors) > 0 { + t.Fatalf("expected the value %q to be either RFC 3339 or positive integer, got error %q", f, errors) + } + } + + invalidT := []string{ + "2018-03-01T00:00:00", // No time zone + "ABC", + "-789", + } + + for _, f := range invalidT { + _, errors := validateTypeStringIsDateOrPositiveInt(f, "parameter") + if len(errors) == 0 { + t.Fatalf("expected the value %q to fail validation", f) + } + } +} diff --git a/website/docs/d/acmpca_certificate.html.markdown b/website/docs/d/acmpca_certificate.html.markdown new file mode 100644 index 00000000000..f356c75636b --- /dev/null +++ b/website/docs/d/acmpca_certificate.html.markdown @@ -0,0 +1,34 @@ +--- +subcategory: "ACM PCA" +layout: "aws" +page_title: "AWS: aws_acmpca_certificate" +description: |- + Get information on a Certificate issued by a AWS Certificate Manager Private Certificate Authority +--- + +# Data Source: aws_acmpca_certificate + +Get information on a Certificate issued by a AWS Certificate Manager Private Certificate Authority. + +## Example Usage + +```terraform +data "aws_acmpca_certificate" "example" { + arn = "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012/certificate/1234b4a0d73e2056789bdbe77d5b1a23" + certificate_authority_arn = "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `arn` - (Required) Amazon Resource Name (ARN) of the certificate issued by the private certificate authority. +* `certificate_authority_arn` - (Required) Amazon Resource Name (ARN) of the certificate authority. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `certificate` - The PEM-encoded certificate value. +* `certificate_chain` - The PEM-encoded certificate chain that includes any intermediate certificates and chains up to root CA. diff --git a/website/docs/r/acm_certificate.html.markdown b/website/docs/r/acm_certificate.html.markdown index 7e81a6b83a1..632596cc772 100644 --- a/website/docs/r/acm_certificate.html.markdown +++ b/website/docs/r/acm_certificate.html.markdown @@ -114,7 +114,7 @@ The following arguments are supported: * `certificate_chain` - (Optional) The certificate's PEM-formatted chain * Creating a private CA issued certificate * `domain_name` - (Required) A domain name for which the certificate should be issued - * `certificate_authority_arn` - (Required) ARN of an ACMPCA + * `certificate_authority_arn` - (Required) ARN of an ACM PCA * `subject_alternative_names` - (Optional) Set of domains that should be SANs in the issued certificate. To remove all elements of a previously configured list, set this value equal to an empty list (`[]`) or use the [`terraform taint` command](https://www.terraform.io/docs/commands/taint.html) to trigger recreation. * `tags` - (Optional) A map of tags to assign to the resource. diff --git a/website/docs/r/acmpca_certificate.html.markdown b/website/docs/r/acmpca_certificate.html.markdown new file mode 100644 index 00000000000..1088ee2135f --- /dev/null +++ b/website/docs/r/acmpca_certificate.html.markdown @@ -0,0 +1,81 @@ +--- +subcategory: "ACM PCA" +layout: "aws" +page_title: "AWS: aws_acmpca_certificate" +description: |- + Provides a resource to issue a certificate using AWS Certificate Manager Private Certificate Authority (ACM PCA) +--- + +# Resource: aws_acmpca_certificate + +Provides a resource to issue a certificate using AWS Certificate Manager Private Certificate Authority (ACM PCA). + +## Example Usage + +### Basic + +```terraform +resource "aws_acmpca_certificate" "example" { + certificate_authority_arn = aws_acmpca_certificate_authority.example.arn + certificate_signing_request = tls_cert_request.csr.cert_request_pem + signing_algorithm = "SHA256WITHRSA" + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "example" { + private_certificate_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "example.com" + } + } + + permanent_deletion_time_in_days = 7 +} + +resource "tls_private_key" "key" { + algorithm = "RSA" +} + +resource "tls_cert_request" "csr" { + key_algorithm = "RSA" + private_key_pem = tls_private_key.key.private_key_pem + + subject { + common_name = "example" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `certificate_authority_arn` - (Required) Amazon Resource Name (ARN) of the certificate authority. +* `certificate_signing_request` - (Required) Certificate Signing Request in PEM format. +* `signing_algorithm` - (Required) Algorithm to use to sign certificate requests. Valid values: `SHA256WITHRSA`, `SHA256WITHECDSA`, `SHA384WITHRSA`, `SHA384WITHECDSA`, `SHA512WITHRSA`, `SHA512WITHECDSA` +* `validity` - (Required) Configures end of the validity period for the certificate. See [validity block](#validity-block) below. +* `template_arn` - (Optional) The template to use when issuing a certificate. See [ACM PCA Documentation](https://docs.aws.amazon.com/acm-pca/latest/userguide/UsingTemplates.html) for more information. + +### validity block + +* `type` - (Required) Determines how `value` is interpreted. Valid values: `DAYS`, `MONTHS`, `YEARS`, `ABSOLUTE`, `END_DATE`. +* `value` - (Required) If `type` is `DAYS`, `MONTHS`, or `YEARS`, the relative time until the certificate expires. If `type` is `ABSOLUTE`, the date in seconds since the Unix epoch. If `type` is `END_DATE`, the date in RFC 3339 format. + + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the certificate. +* `certificate` - The PEM-encoded certificate value. +* `certificate_chain` - The PEM-encoded certificate chain that includes any intermediate certificates and chains up to root CA. + +## Import + +`aws_acmpca_certificate` can not be imported at this time. diff --git a/website/docs/r/acmpca_certificate_authority_certificate.html.markdown b/website/docs/r/acmpca_certificate_authority_certificate.html.markdown new file mode 100644 index 00000000000..9cfe798fc48 --- /dev/null +++ b/website/docs/r/acmpca_certificate_authority_certificate.html.markdown @@ -0,0 +1,113 @@ +--- +subcategory: "ACM PCA" +layout: "aws" +page_title: "AWS: aws_acmpca_certificate_authority_certificate" +description: |- + Associates a certificate with an AWS Certificate Manager Private Certificate Authority +--- + +# Resource: aws_acmpca_certificate_authority_certificate + +Associates a certificate with an AWS Certificate Manager Private Certificate Authority (ACM PCA Certificate Authority). An ACM PCA Certificate Authority is unable to issue certificates until it has a certificate associated with it. A root level ACM PCA Certificate Authority is able to self-sign its own root certificate. + +## Example Usage + +### Self-Signed Root Certificate Authority Certificate + +```terraform +resource "aws_acmpca_certificate_authority_certificate" "example" { + certificate_authority_arn = aws_acmpca_certificate_authority.example.arn + + certificate = aws_acmpca_certificate.example.certificate + certificate_chain = aws_acmpca_certificate.example.certificate_chain +} + +resource "aws_acmpca_certificate" "example" { + certificate_authority_arn = aws_acmpca_certificate_authority.example.arn + certificate_signing_request = aws_acmpca_certificate_authority.example.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "example" { + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "example.com" + } + } +} + +data "aws_partition" "current" {} +``` + +### Certificate for Subordinate Certificate Authority + +Note that the certificate for the subordinate certificate authority must be issued by the root certificate authority using a signing request from the subordinate certificate authority. + +```terraform +resource "aws_acmpca_certificate_authority_certificate" "subordinate" { + certificate_authority_arn = aws_acmpca_certificate_authority.subordinate.arn + + certificate = aws_acmpca_certificate.subordinate.certificate + certificate_chain = aws_acmpca_certificate.subordinate.certificate_chain +} + +resource "aws_acmpca_certificate" "subordinate" { + certificate_authority_arn = aws_acmpca_certificate_authority.root.arn + certificate_signing_request = aws_acmpca_certificate_authority.subordinate.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority" "subordinate" { + type = "SUBORDINATE" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "sub.example.com" + } + } +} + +resource "aws_acmpca_certificate_authority" "root" { + # ... +} + +resource "aws_acmpca_certificate_authority_certificate" "root" { + # ... +} + +resource "aws_acmpca_certificate" "root" { + # ... +} + +data "aws_partition" "current" {} +``` + +## Argument Reference + +The following arguments are supported: + +* `certificate` - (Required) The PEM-encoded certificate for the Certificate Authority. +* `certificate_authority_arn` - (Required) Amazon Resource Name (ARN) of the Certificate Authority. +* `certificate_chain` - (Optional) The PEM-encoded certificate chain that includes any intermediate certificates and chains up to root CA. Required for subordinate Certificate Authorities. Not allowed for root Certificate Authorities.