diff --git a/.changelog/8114.txt b/.changelog/8114.txt new file mode 100644 index 00000000000..4538eb4af9a --- /dev/null +++ b/.changelog/8114.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cognito_user_pool_ui_customization +``` diff --git a/aws/internal/service/cognitoidentityprovider/finder/finder.go b/aws/internal/service/cognitoidentityprovider/finder/finder.go new file mode 100644 index 00000000000..692a7bad474 --- /dev/null +++ b/aws/internal/service/cognitoidentityprovider/finder/finder.go @@ -0,0 +1,35 @@ +package finder + +import ( + "reflect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" +) + +// CognitoUserPoolUICustomization returns the UI Customization corresponding to the UserPoolId and ClientId. +// Returns nil if no UI Customization is found. +func CognitoUserPoolUICustomization(conn *cognitoidentityprovider.CognitoIdentityProvider, userPoolId, clientId string) (*cognitoidentityprovider.UICustomizationType, error) { + input := &cognitoidentityprovider.GetUICustomizationInput{ + ClientId: aws.String(clientId), + UserPoolId: aws.String(userPoolId), + } + + output, err := conn.GetUICustomization(input) + + if err != nil { + return nil, err + } + + if output == nil || output.UICustomization == nil { + return nil, nil + } + + // The GetUICustomization API operation will return an empty struct + // if nothing is present rather than nil or an error, so we equate that with nil + if reflect.DeepEqual(output.UICustomization, &cognitoidentityprovider.UICustomizationType{}) { + return nil, nil + } + + return output.UICustomization, nil +} diff --git a/aws/provider.go b/aws/provider.go index d25e53dab6a..eb32094a3a3 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -521,13 +521,14 @@ func Provider() *schema.Provider { "aws_cognito_identity_pool": resourceAwsCognitoIdentityPool(), "aws_cognito_identity_pool_roles_attachment": resourceAwsCognitoIdentityPoolRolesAttachment(), "aws_cognito_identity_provider": resourceAwsCognitoIdentityProvider(), + "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), "aws_cognito_user_group": resourceAwsCognitoUserGroup(), "aws_cognito_user_pool": resourceAwsCognitoUserPool(), "aws_cognito_user_pool_client": resourceAwsCognitoUserPoolClient(), "aws_cognito_user_pool_domain": resourceAwsCognitoUserPoolDomain(), + "aws_cognito_user_pool_ui_customization": resourceAwsCognitoUserPoolUICustomization(), "aws_cloudhsm_v2_cluster": resourceAwsCloudHsmV2Cluster(), "aws_cloudhsm_v2_hsm": resourceAwsCloudHsmV2Hsm(), - "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), "aws_cloudwatch_composite_alarm": resourceAwsCloudWatchCompositeAlarm(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_cloudwatch_dashboard": resourceAwsCloudWatchDashboard(), diff --git a/aws/resource_aws_cognito_user_pool_ui_customization.go b/aws/resource_aws_cognito_user_pool_ui_customization.go new file mode 100644 index 00000000000..6d80f256875 --- /dev/null +++ b/aws/resource_aws_cognito_user_pool_ui_customization.go @@ -0,0 +1,187 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cognitoidentityprovider/finder" +) + +func resourceAwsCognitoUserPoolUICustomization() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCognitoUserPoolUICustomizationPut, + Read: resourceAwsCognitoUserPoolUICustomizationRead, + Update: resourceAwsCognitoUserPoolUICustomizationPut, + Delete: resourceAwsCognitoUserPoolUICustomizationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Optional: true, + Default: "ALL", + }, + + "creation_date": { + Type: schema.TypeString, + Computed: true, + }, + + "css": { + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"css", "image_file"}, + }, + + "css_version": { + Type: schema.TypeString, + Computed: true, + }, + + "image_file": { + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"image_file", "css"}, + }, + + "image_url": { + Type: schema.TypeString, + Computed: true, + }, + + "last_modified_date": { + Type: schema.TypeString, + Computed: true, + }, + + "user_pool_id": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceAwsCognitoUserPoolUICustomizationPut(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + clientId := d.Get("client_id").(string) + userPoolId := d.Get("user_pool_id").(string) + + input := &cognitoidentityprovider.SetUICustomizationInput{ + ClientId: aws.String(clientId), + UserPoolId: aws.String(userPoolId), + } + + if v, ok := d.GetOk("css"); ok { + input.CSS = aws.String(v.(string)) + } + + if v, ok := d.GetOk("image_file"); ok { + imgFile, err := base64.StdEncoding.DecodeString(v.(string)) + if err != nil { + return fmt.Errorf("error Base64 decoding image file for Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s): %w", userPoolId, clientId, err) + } + + input.ImageFile = imgFile + } + + _, err := conn.SetUICustomization(input) + + if err != nil { + return fmt.Errorf("error setting Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s): %w", userPoolId, clientId, err) + } + + d.SetId(fmt.Sprintf("%s,%s", userPoolId, clientId)) + + return resourceAwsCognitoUserPoolUICustomizationRead(d, meta) +} + +func resourceAwsCognitoUserPoolUICustomizationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + userPoolId, clientId, err := parseCognitoUserPoolUICustomizationID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Cognito User Pool UI customization ID (%s): %w", d.Id(), err) + } + + uiCustomization, err := finder.CognitoUserPoolUICustomization(conn, userPoolId, clientId) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, cognitoidentityprovider.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s) not found, removing from state", userPoolId, clientId) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s): %w", userPoolId, clientId, err) + } + + if uiCustomization == nil { + if d.IsNewResource() { + return fmt.Errorf("error getting Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s): not found", userPoolId, clientId) + } + + log.Printf("[WARN] Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s) not found, removing from state", userPoolId, clientId) + d.SetId("") + return nil + } + + d.Set("client_id", uiCustomization.ClientId) + d.Set("creation_date", aws.TimeValue(uiCustomization.CreationDate).Format(time.RFC3339)) + d.Set("css", uiCustomization.CSS) + d.Set("css_version", uiCustomization.CSSVersion) + d.Set("image_url", uiCustomization.ImageUrl) + d.Set("last_modified_date", aws.TimeValue(uiCustomization.LastModifiedDate).Format(time.RFC3339)) + d.Set("user_pool_id", uiCustomization.UserPoolId) + + return nil +} + +func resourceAwsCognitoUserPoolUICustomizationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + userPoolId, clientId, err := parseCognitoUserPoolUICustomizationID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Cognito User Pool UI customization ID (%s): %w", d.Id(), err) + } + + input := &cognitoidentityprovider.SetUICustomizationInput{ + ClientId: aws.String(clientId), + UserPoolId: aws.String(userPoolId), + } + + _, err = conn.SetUICustomization(input) + + if tfawserr.ErrCodeEquals(err, cognitoidentityprovider.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Cognito User Pool UI customization (UserPoolId: %s, ClientId: %s): %w", userPoolId, clientId, err) + } + + return nil +} + +func parseCognitoUserPoolUICustomizationID(id string) (string, string, error) { + idParts := strings.SplitN(id, ",", 2) + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return "", "", fmt.Errorf("please make sure ID is in format USER_POOL_ID,CLIENT_ID") + } + + return idParts[0], idParts[1], nil +} diff --git a/aws/resource_aws_cognito_user_pool_ui_customization_test.go b/aws/resource_aws_cognito_user_pool_ui_customization_test.go new file mode 100644 index 00000000000..03c5611633a --- /dev/null +++ b/aws/resource_aws_cognito_user_pool_ui_customization_test.go @@ -0,0 +1,734 @@ +package aws + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cognitoidentityprovider/finder" +) + +func TestAccAWSCognitoUserPoolUICustomization_AllClients_CSS(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + css := ".label-customizable {font-weight: 400;}" + cssUpdated := ".label-customizable {font-weight: 100;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, cssUpdated), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", cssUpdated), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_AllClients_Disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + + css := ".label-customizable {font-weight: 400;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCognitoIdentityProvider(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsCognitoUserPoolUICustomization(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_AllClients_ImageFile(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + filename := "testdata/service/cognitoidentityprovider/logo.png" + updatedFilename := "testdata/service/cognitoidentityprovider/logo_modified.png" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_Image(rName, filename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_Image(rName, updatedFilename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_AllClients_CSSAndImageFile(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + css := ".label-customizable {font-weight: 400;}" + filename := "testdata/service/cognitoidentityprovider/logo.png" + updatedFilename := "testdata/service/cognitoidentityprovider/logo_modified.png" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSSAndImage(rName, css, filename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_Image(rName, updatedFilename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_Client_CSS(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + clientResourceName := "aws_cognito_user_pool_client.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + css := ".label-customizable {font-weight: 400;}" + cssUpdated := ".label-customizable {font-weight: 100;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, cssUpdated), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", cssUpdated), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "css_version"), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_Client_Disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + + css := ".label-customizable {font-weight: 400;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSCognitoIdentityProvider(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsCognitoUserPoolUICustomization(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_Client_Image(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + clientResourceName := "aws_cognito_user_pool_client.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + filename := "testdata/service/cognitoidentityprovider/logo.png" + updatedFilename := "testdata/service/cognitoidentityprovider/logo_modified.png" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_Image(rName, filename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_Image(rName, updatedFilename), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "image_url"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_file"}, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_ClientAndAll_CSS(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.ui_all" + clientUIResourceName := "aws_cognito_user_pool_ui_customization.ui_client" + + clientResourceName := "aws_cognito_user_pool_client.test" + userPoolResourceName := "aws_cognito_user_pool.test" + + allCSS := ".label-customizable {font-weight: 400;}" + clientCSS := ".label-customizable {font-weight: 100;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + // Test UI Customization settings shared by ALL and a specific client + Config: testAccAWSCognitoUserPoolUICustomizationConfig_ClientAndAllCustomizations_CSS(rName, allCSS, allCSS), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + testAccCheckAWSCognitoUserPoolUICustomizationExists(clientUIResourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "css", allCSS), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + resource.TestCheckResourceAttrPair(clientUIResourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(clientUIResourceName, "creation_date"), + resource.TestCheckResourceAttr(clientUIResourceName, "css", allCSS), + resource.TestCheckResourceAttrSet(clientUIResourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(clientUIResourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: clientUIResourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // Test UI Customization settings overridden for the client + Config: testAccAWSCognitoUserPoolUICustomizationConfig_ClientAndAllCustomizations_CSS(rName, allCSS, clientCSS), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + testAccCheckAWSCognitoUserPoolUICustomizationExists(clientUIResourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttr(resourceName, "css", allCSS), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(resourceName, "user_pool_id", userPoolResourceName, "id"), + resource.TestCheckResourceAttrPair(clientUIResourceName, "client_id", clientResourceName, "id"), + resource.TestCheckResourceAttrSet(clientUIResourceName, "creation_date"), + resource.TestCheckResourceAttr(clientUIResourceName, "css", clientCSS), + resource.TestCheckResourceAttrSet(clientUIResourceName, "last_modified_date"), + resource.TestCheckResourceAttrPair(clientUIResourceName, "user_pool_id", userPoolResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: clientUIResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_UpdateClientToAll_CSS(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + clientResourceName := "aws_cognito_user_pool_client.test" + + css := ".label-customizable {font-weight: 100;}" + cssUpdated := ".label-customizable {font-weight: 400;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + ), + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, cssUpdated), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", cssUpdated), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSCognitoUserPoolUICustomization_UpdateAllToClient_CSS(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_cognito_user_pool_ui_customization.test" + clientResourceName := "aws_cognito_user_pool_client.test" + + css := ".label-customizable {font-weight: 100;}" + cssUpdated := ".label-customizable {font-weight: 400;}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCognitoUserPoolUICustomizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, css), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", css), + resource.TestCheckResourceAttr(resourceName, "client_id", "ALL"), + ), + }, + { + Config: testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, cssUpdated), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSCognitoUserPoolUICustomizationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "css", cssUpdated), + resource.TestCheckResourceAttrPair(resourceName, "client_id", clientResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSCognitoUserPoolUICustomizationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cognitoidpconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cognito_user_pool_ui_customization" { + continue + } + + userPoolId, clientId, err := parseCognitoUserPoolUICustomizationID(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing Cognito User Pool UI customization ID (%s): %w", rs.Primary.ID, err) + } + + output, err := finder.CognitoUserPoolUICustomization(conn, userPoolId, clientId) + + if tfawserr.ErrCodeEquals(err, cognitoidentityprovider.ErrCodeResourceNotFoundException) { + continue + } + + // Catch cases where the User Pool Domain has been destroyed, effectively eliminating + // a UI customization; calls to GetUICustomization will fail + if tfawserr.ErrMessageContains(err, cognitoidentityprovider.ErrCodeInvalidParameterException, "There has to be an existing domain associated with this user pool") { + continue + } + + if err != nil { + return err + } + + if testAccAWSCognitoUserPoolUICustomizationExists(output) { + return fmt.Errorf("Cognito User Pool UI Customization (UserPoolId: %s, ClientId: %s) still exists", userPoolId, clientId) + } + } + + return nil +} + +func testAccCheckAWSCognitoUserPoolUICustomizationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return errors.New("No Cognito User Pool Client ID set") + } + + userPoolId, clientId, err := parseCognitoUserPoolUICustomizationID(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing Cognito User Pool UI customization ID (%s): %w", rs.Primary.ID, err) + } + + conn := testAccProvider.Meta().(*AWSClient).cognitoidpconn + + output, err := finder.CognitoUserPoolUICustomization(conn, userPoolId, clientId) + + if err != nil { + return err + } + + if output == nil { + return fmt.Errorf("Cognito User Pool UI customization (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSS(rName, css string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "test" { + css = %q + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, css) +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_Image(rName, filename string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "test" { + image_file = filebase64(%q) + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, filename) +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_AllClients_CSSAndImage(rName, css, filename string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "test" { + css = %q + image_file = filebase64(%q) + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, css, filename) +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_Client_CSS(rName, css string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "test" { + client_id = aws_cognito_user_pool_client.test.id + css = %q + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, css) +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_Client_Image(rName, filename string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "test" { + client_id = aws_cognito_user_pool_client.test.id + image_file = filebase64(%q) + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, filename) +} + +func testAccAWSCognitoUserPoolUICustomizationConfig_ClientAndAllCustomizations_CSS(rName, allCSS, clientCSS string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user_pool_domain" "test" { + domain = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[1]q + user_pool_id = aws_cognito_user_pool.test.id +} + +resource "aws_cognito_user_pool_ui_customization" "ui_all" { + css = %q + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} + +resource "aws_cognito_user_pool_ui_customization" "ui_client" { + client_id = aws_cognito_user_pool_client.test.id + css = %q + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.test.user_pool_id +} +`, rName, allCSS, clientCSS) +} + +// testAccAWSCognitoUserPoolUICustomizationExists validates the API object such that +// we define resource existence when the object is non-nil and +// at least one of the object's fields are non-nil with the exception of CSSVersion +// which remains as an artifact even after UI customization removal +func testAccAWSCognitoUserPoolUICustomizationExists(ui *cognitoidentityprovider.UICustomizationType) bool { + if ui == nil { + return false + } + + if ui.CSS != nil { + return true + } + + if ui.CreationDate != nil { + return true + } + + if ui.ImageUrl != nil { + return true + } + + if ui.LastModifiedDate != nil { + return true + } + + return false +} diff --git a/aws/testdata/service/cognitoidentityprovider/logo.png b/aws/testdata/service/cognitoidentityprovider/logo.png new file mode 100644 index 00000000000..d8348fa7be7 Binary files /dev/null and b/aws/testdata/service/cognitoidentityprovider/logo.png differ diff --git a/aws/testdata/service/cognitoidentityprovider/logo_modified.png b/aws/testdata/service/cognitoidentityprovider/logo_modified.png new file mode 100644 index 00000000000..71db86115cb Binary files /dev/null and b/aws/testdata/service/cognitoidentityprovider/logo_modified.png differ diff --git a/website/docs/r/cognito_user_pool_ui_customization.html.markdown b/website/docs/r/cognito_user_pool_ui_customization.html.markdown new file mode 100644 index 00000000000..cca7ca7da7d --- /dev/null +++ b/website/docs/r/cognito_user_pool_ui_customization.html.markdown @@ -0,0 +1,92 @@ +--- +subcategory: "Cognito" +layout: "aws" +page_title: "AWS: aws_cognito_user_pool_ui_customization" +description: |- + Provides a Cognito User Pool UI Customization resource. +--- + +# Resource: aws_cognito_user_pool_ui_customization + +Provides a Cognito User Pool UI Customization resource. + +~> **Note:** To use this resource, the user pool must have a domain associated with it. For more information, see the Amazon Cognito Developer Guide on [Customizing the Built-in Sign-In and Sign-up Webpages](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-ui-customization.html). + +### Example Usage + +### UI customization settings for a single client + +```hcl +resource "aws_cognito_user_pool" "example" { + name = "example" +} + +resource "aws_cognito_user_pool_domain" "example" { + domain = "example" + user_pool_id = aws_cognito_user_pool.example.id +} + +resource "aws_cognito_user_pool_client" "example" { + name = "example" + user_pool_id = aws_cognito_user_pool.example.id +} + +resource "aws_cognito_user_pool_ui_customization" "example" { + client_id = aws_cognito_user_pool_client.example.id + + css = ".label-customizable {font-weight: 400;}" + image_file = filebase64("logo.png") + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.example.user_pool_id +} +``` + +### UI customization settings for all clients + +```hcl +resource "aws_cognito_user_pool" "example" { + name = "example" +} + +resource "aws_cognito_user_pool_domain" "example" { + domain = "example" + user_pool_id = aws_cognito_user_pool.example.id +} + +resource "aws_cognito_user_pool_ui_customization" "example" { + css = ".label-customizable {font-weight: 400;}" + image_file = filebase64("logo.png") + + # Refer to the aws_cognito_user_pool_domain resource's + # user_pool_id attribute to ensure it is in an 'Active' state + user_pool_id = aws_cognito_user_pool_domain.example.user_pool_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `client_id` (Optional) The client ID for the client app. Defaults to `ALL`. If `ALL` is specified, the `css` and/or `image_file` settings will be used for every client that has no UI customization set previously. +* `css` (Optional) - The CSS values in the UI customization, provided as a String. At least one of `css` or `image_file` is required. +* `image_file` (Optional) - The uploaded logo image for the UI customization, provided as a base64-encoded String. Drift detection is not possible for this argument. At least one of `css` or `image_file` is required. +* `user_pool_id` (Required) - The user pool ID for the user pool. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `creation_date` - The creation date in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8) for the UI customization. +* `css_version` - The CSS version number. +* `image_url` - The logo image URL for the UI customization. +* `last_modified_date` - The last-modified date in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8) for the UI customization. + +## Import + +Cognito User Pool UI Customizations can be imported using the `user_pool_id` and `client_id` separated by `,`, e.g. + +``` +$ terraform import aws_cognito_user_pool_ui_customization.example us-west-2_ZCTarbt5C,12bu4fuk3mlgqa2rtrujgp6egq +```