diff --git a/.changelog/23161.txt b/.changelog/23161.txt new file mode 100644 index 00000000000..8805ba5bb8d --- /dev/null +++ b/.changelog/23161.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_iam_signing_certificate +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d3b00076b04..bb467089cb5 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1406,6 +1406,7 @@ func Provider() *schema.Provider { "aws_iam_server_certificate": iam.ResourceServerCertificate(), "aws_iam_service_linked_role": iam.ResourceServiceLinkedRole(), "aws_iam_service_specific_credential": iam.ResourceServiceSpecificCredential(), + "aws_iam_signing_certificate": iam.ResourceSigningCertificate(), "aws_iam_user": iam.ResourceUser(), "aws_iam_user_group_membership": iam.ResourceUserGroupMembership(), "aws_iam_user_login_profile": iam.ResourceUserLoginProfile(), diff --git a/internal/service/iam/find.go b/internal/service/iam/find.go index 7484623d3c6..0033e8932f6 100644 --- a/internal/service/iam/find.go +++ b/internal/service/iam/find.go @@ -241,3 +241,42 @@ func FindServiceSpecificCredential(conn *iam.IAM, serviceName, userName, credID return cred, nil } + +func FindSigningCertificate(conn *iam.IAM, userName, certId string) (*iam.SigningCertificate, error) { + input := &iam.ListSigningCertificatesInput{ + UserName: aws.String(userName), + } + + output, err := conn.ListSigningCertificates(input) + + if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if len(output.Certificates) == 0 || output.Certificates[0] == nil { + return nil, tfresource.NewEmptyResultError(output) + } + + var cert *iam.SigningCertificate + + for _, crt := range output.Certificates { + if aws.StringValue(crt.UserName) == userName && + aws.StringValue(crt.CertificateId) == certId { + cert = crt + break + } + } + + if cert == nil { + return nil, tfresource.NewEmptyResultError(cert) + } + + return cert, nil +} diff --git a/internal/service/iam/signing_certificate.go b/internal/service/iam/signing_certificate.go new file mode 100644 index 00000000000..8c48a70709c --- /dev/null +++ b/internal/service/iam/signing_certificate.go @@ -0,0 +1,176 @@ +package iam + +import ( // nosemgrep: aws-sdk-go-multiple-service-imports + + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func ResourceSigningCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceSigningCertificateCreate, + Read: resourceSigningCertificateRead, + Update: resourceSigningCertificateUpdate, + Delete: resourceSigningCertificateDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "certificate_body": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: suppressNormalizeCertRemoval, + }, + "certificate_id": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: iam.StatusTypeActive, + ValidateFunc: validation.StringInSlice(iam.StatusType_Values(), false), + }, + "user_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceSigningCertificateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).IAMConn + + createOpts := &iam.UploadSigningCertificateInput{ + CertificateBody: aws.String(d.Get("certificate_body").(string)), + UserName: aws.String(d.Get("user_name").(string)), + } + + log.Printf("[DEBUG] Creating IAM Signing Certificate with opts: %s", createOpts) + resp, err := conn.UploadSigningCertificate(createOpts) + if err != nil { + return fmt.Errorf("error uploading IAM Signing Certificate: %w", err) + } + + cert := resp.Certificate + certId := cert.CertificateId + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(certId), aws.StringValue(cert.UserName))) + + if v, ok := d.GetOk("status"); ok && v.(string) != iam.StatusTypeActive { + updateInput := &iam.UpdateSigningCertificateInput{ + CertificateId: certId, + UserName: aws.String(d.Get("user_name").(string)), + Status: aws.String(v.(string)), + } + + _, err := conn.UpdateSigningCertificate(updateInput) + if err != nil { + return fmt.Errorf("error settings IAM Signing Certificate status: %w", err) + } + } + + return resourceSigningCertificateRead(d, meta) +} + +func resourceSigningCertificateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).IAMConn + + certId, userName, err := DecodeSigningCertificateId(d.Id()) + if err != nil { + return err + } + + outputRaw, err := tfresource.RetryWhenNewResourceNotFound(PropagationTimeout, func() (interface{}, error) { + return FindSigningCertificate(conn, userName, certId) + }, d.IsNewResource()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] IAM Signing Certificate (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading IAM Signing Certificate (%s): %w", d.Id(), err) + } + + resp := outputRaw.(*iam.SigningCertificate) + + d.Set("certificate_body", resp.CertificateBody) + d.Set("certificate_id", resp.CertificateId) + d.Set("user_name", resp.UserName) + d.Set("status", resp.Status) + + return nil +} + +func resourceSigningCertificateUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).IAMConn + + certId, userName, err := DecodeSigningCertificateId(d.Id()) + if err != nil { + return err + } + + updateInput := &iam.UpdateSigningCertificateInput{ + CertificateId: aws.String(certId), + UserName: aws.String(userName), + Status: aws.String(d.Get("status").(string)), + } + + _, err = conn.UpdateSigningCertificate(updateInput) + if err != nil { + return fmt.Errorf("error updating IAM Signing Certificate: %w", err) + } + + return resourceSigningCertificateRead(d, meta) +} + +func resourceSigningCertificateDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).IAMConn + log.Printf("[INFO] Deleting IAM Signing Certificate: %s", d.Id()) + + certId, userName, err := DecodeSigningCertificateId(d.Id()) + if err != nil { + return err + } + + input := &iam.DeleteSigningCertificateInput{ + CertificateId: aws.String(certId), + UserName: aws.String(userName), + } + + if _, err := conn.DeleteSigningCertificate(input); err != nil { + if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return nil + } + return fmt.Errorf("Error deleting IAM Signing Certificate %s: %s", d.Id(), err) + } + + return nil +} + +func DecodeSigningCertificateId(id string) (string, string, error) { + creds := strings.Split(id, ":") + if len(creds) != 2 { + return "", "", fmt.Errorf("unknown IAM Signing Certificate ID format") + } + + certId := creds[0] + userName := creds[1] + + return certId, userName, nil +} diff --git a/internal/service/iam/signing_certificate_test.go b/internal/service/iam/signing_certificate_test.go new file mode 100644 index 00000000000..48303370315 --- /dev/null +++ b/internal/service/iam/signing_certificate_test.go @@ -0,0 +1,202 @@ +package iam_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/iam" + sdkacctest "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/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccSigningCertificate_basic(t *testing.T) { + var cred iam.SigningCertificate + + resourceName := "aws_iam_signing_certificate.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + key := acctest.TLSRSAPrivateKeyPEM(2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckSigningCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSigningCertificateBasicConfig(rName, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckSigningCertificateExists(resourceName, &cred), + resource.TestCheckResourceAttrPair(resourceName, "user_name", "aws_iam_user.test", "name"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_id"), + resource.TestCheckResourceAttrSet(resourceName, "certificate_body"), + resource.TestCheckResourceAttr(resourceName, "status", "Active"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSigningCertificate_status(t *testing.T) { + var cred iam.SigningCertificate + + resourceName := "aws_iam_signing_certificate.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + key := acctest.TLSRSAPrivateKeyPEM(2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckSigningCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSigningCertificateConfigStatus(rName, "Inactive", certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckSigningCertificateExists(resourceName, &cred), + resource.TestCheckResourceAttr(resourceName, "status", "Inactive"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSigningCertificateConfigStatus(rName, "Active", certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckSigningCertificateExists(resourceName, &cred), + resource.TestCheckResourceAttr(resourceName, "status", "Active"), + ), + }, + { + Config: testAccSigningCertificateConfigStatus(rName, "Inactive", certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckSigningCertificateExists(resourceName, &cred), + resource.TestCheckResourceAttr(resourceName, "status", "Inactive"), + ), + }, + }, + }) +} + +func TestAccSigningCertificate_disappears(t *testing.T) { + var cred iam.SigningCertificate + resourceName := "aws_iam_signing_certificate.test" + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + key := acctest.TLSRSAPrivateKeyPEM(2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckSigningCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSigningCertificateBasicConfig(rName, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckSigningCertificateExists(resourceName, &cred), + acctest.CheckResourceDisappears(acctest.Provider, tfiam.ResourceSigningCertificate(), resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfiam.ResourceSigningCertificate(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckSigningCertificateExists(n string, cred *iam.SigningCertificate) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Server Cert ID is set") + } + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMConn + + certId, userName, err := tfiam.DecodeSigningCertificateId(rs.Primary.ID) + if err != nil { + return err + } + + output, err := tfiam.FindSigningCertificate(conn, userName, certId) + if err != nil { + return err + } + + *cred = *output + + return nil + } +} + +func testAccCheckSigningCertificateDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_signing_certificate" { + continue + } + + certId, userName, err := tfiam.DecodeSigningCertificateId(rs.Primary.ID) + if err != nil { + return err + } + + output, err := tfiam.FindSigningCertificate(conn, userName, certId) + + if tfresource.NotFound(err) { + continue + } + + if output != nil { + return fmt.Errorf("IAM Service Specific Credential (%s) still exists", rs.Primary.ID) + } + + } + + return nil +} + +func testAccSigningCertificateBasicConfig(rName, cert string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "test" { + name = %[1]q +} + +resource "aws_iam_signing_certificate" "test" { + certificate_body = "%[2]s" + user_name = aws_iam_user.test.name +} +`, rName, acctest.TLSPEMEscapeNewlines(cert)) +} + +func testAccSigningCertificateConfigStatus(rName, status, cert string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "test" { + name = %[1]q +} + +resource "aws_iam_signing_certificate" "test" { + certificate_body = "%[3]s" + user_name = aws_iam_user.test.name + status = %[2]q +} +`, rName, status, acctest.TLSPEMEscapeNewlines(cert)) +} diff --git a/internal/service/iam/sweep.go b/internal/service/iam/sweep.go index 176422ae627..dcbbd11b443 100644 --- a/internal/service/iam/sweep.go +++ b/internal/service/iam/sweep.go @@ -86,6 +86,11 @@ func init() { F: sweepServiceSpecificCredentials, }) + resource.AddTestSweepers("aws_iam_signing_certificate", &resource.Sweeper{ + Name: "aws_iam_signing_certificate", + F: sweepSigningCertificates, + }) + resource.AddTestSweepers("aws_iam_server_certificate", &resource.Sweeper{ Name: "aws_iam_server_certificate", F: sweepServerCertificates, @@ -102,6 +107,7 @@ func init() { Dependencies: []string{ "aws_iam_service_specific_credential", "aws_iam_virtual_mfa_device", + "aws_iam_signing_certificate", }, }) @@ -910,3 +916,69 @@ func sweepVirtualMfaDevice(region string) error { return sweeperErrs.ErrorOrNil() } + +func sweepSigningCertificates(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*conns.AWSClient).IAMConn + + var sweeperErrs *multierror.Error + + prefixes := []string{ + "test-user", + "test_user", + "tf-acc", + "tf_acc", + } + + users := make([]*iam.User, 0) + + err = conn.ListUsersPages(&iam.ListUsersInput{}, func(page *iam.ListUsersOutput, lastPage bool) bool { + for _, user := range page.Users { + for _, prefix := range prefixes { + if strings.HasPrefix(aws.StringValue(user.UserName), prefix) { + users = append(users, user) + break + } + } + } + + return !lastPage + }) + + for _, user := range users { + out, err := conn.ListSigningCertificates(&iam.ListSigningCertificatesInput{ + UserName: user.UserName, + }) + + for _, cert := range out.Certificates { + + id := fmt.Sprintf("%s:%s", aws.StringValue(cert.CertificateId), aws.StringValue(cert.UserName)) + + r := ResourceSigningCertificate() + d := r.Data(nil) + d.SetId(id) + err := r.Delete(d, client) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting IAM Signing Certificate (%s): %w", id, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping IAM Signing Certificate sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error describing IAM Signing Certificates: %w", err)) + } + } + + return sweeperErrs.ErrorOrNil() +} diff --git a/internal/service/iam/user_test.go b/internal/service/iam/user_test.go index 7355f7c0764..e94cc204836 100644 --- a/internal/service/iam/user_test.go +++ b/internal/service/iam/user_test.go @@ -2,7 +2,6 @@ package iam_test import ( "fmt" - "os" "testing" "time" @@ -674,12 +673,11 @@ func testAccCheckUserUploadSigningCertificate(getUserOutput *iam.GetUserOutput) return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).IAMConn - signingCertificate, err := os.ReadFile("./test-fixtures/iam-ssl-unix-line-endings.pem") - if err != nil { - return fmt.Errorf("error reading signing certificate fixture: %s", err) - } + key := acctest.TLSRSAPrivateKeyPEM(2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") + input := &iam.UploadSigningCertificateInput{ - CertificateBody: aws.String(string(signingCertificate)), + CertificateBody: aws.String(certificate), UserName: getUserOutput.User.UserName, } diff --git a/website/docs/r/iam_signing_certificate.html.markdown b/website/docs/r/iam_signing_certificate.html.markdown new file mode 100644 index 00000000000..6178042fc58 --- /dev/null +++ b/website/docs/r/iam_signing_certificate.html.markdown @@ -0,0 +1,62 @@ +--- +subcategory: "IAM" +layout: "aws" +page_title: "AWS: aws_iam_signing_certificate" +description: |- + Provides an IAM Signing Certificate +--- + +# Resource: aws_iam_signing_certificate + +Provides an IAM Signing Certificate resource to upload Signing Certificates. + +~> **Note:** All arguments including the certificate body will be stored in the raw state as plain-text. +[Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html). + +## Example Usage + +**Using certs on file:** + +```terraform +resource "aws_iam_signing_certificate" "test_cert" { + username = "some_test_cert" + certificate_body = file("self-ca-cert.pem") +} +``` + +**Example with cert in-line:** + +```terraform +resource "aws_iam_signing_certificate" "test_cert_alt" { + username = "some_test_cert" + + certificate_body = <