diff --git a/.changelog/19919.txt b/.changelog/19919.txt new file mode 100644 index 000000000000..d0ac51811429 --- /dev/null +++ b/.changelog/19919.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cognito_user +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f8ba99415936..fcd909edc155 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -992,6 +992,7 @@ func Provider() *schema.Provider { "aws_cognito_identity_provider": cognitoidp.ResourceIdentityProvider(), "aws_cognito_resource_server": cognitoidp.ResourceResourceServer(), "aws_cognito_user_group": cognitoidp.ResourceUserGroup(), + "aws_cognito_user": cognitoidp.ResourceUser(), "aws_cognito_user_pool": cognitoidp.ResourceUserPool(), "aws_cognito_user_pool_client": cognitoidp.ResourceUserPoolClient(), "aws_cognito_user_pool_domain": cognitoidp.ResourceUserPoolDomain(), diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go new file mode 100644 index 000000000000..3868939fe653 --- /dev/null +++ b/internal/service/cognitoidp/user.go @@ -0,0 +1,537 @@ +package cognitoidp + +import ( + "errors" + "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/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" +) + +func ResourceUser() *schema.Resource { + return &schema.Resource{ + Create: resourceUserCreate, + Read: resourceUserRead, + Update: resourceUserUpdate, + Delete: resourceUserDelete, + + Importer: &schema.ResourceImporter{ + State: resourceUserImport, + }, + + // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html + Schema: map[string]*schema.Schema{ + "attributes": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if k == "attributes.sub" || k == "attributes.%" { + return true + } + + return false + }, + Optional: true, + }, + "client_metadata": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "creation_date": { + Type: schema.TypeString, + Computed: true, + }, + "desired_delivery_mediums": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(cognitoidentityprovider.DeliveryMediumType_Values(), false), + }, + Optional: true, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "force_alias_creation": { + Type: schema.TypeBool, + Optional: true, + }, + "last_modified_date": { + Type: schema.TypeString, + Computed: true, + }, + "message_action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cognitoidentityprovider.MessageActionType_Values(), false), + }, + "mfa_setting_list": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "preferred_mfa_setting": { + Type: schema.TypeString, + Computed: true, + }, + "user_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "sub": { + Type: schema.TypeString, + Computed: true, + }, + "password": { + Type: schema.TypeString, + Sensitive: true, + Optional: true, + ValidateFunc: validation.StringLenBetween(6, 256), + ConflictsWith: []string{"temporary_password"}, + }, + "temporary_password": { + Type: schema.TypeString, + Sensitive: true, + Optional: true, + ValidateFunc: validation.StringLenBetween(6, 256), + ConflictsWith: []string{"password"}, + }, + "validation_data": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + }, + } +} + +func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).CognitoIDPConn + + username := d.Get("username").(string) + userPoolId := d.Get("user_pool_id").(string) + + params := &cognitoidentityprovider.AdminCreateUserInput{ + Username: aws.String(username), + UserPoolId: aws.String(userPoolId), + } + + if v, ok := d.GetOk("client_metadata"); ok { + metadata := v.(map[string]interface{}) + params.ClientMetadata = expandUserClientMetadata(metadata) + } + + if v, ok := d.GetOk("desired_delivery_mediums"); ok { + mediums := v.(*schema.Set) + params.DesiredDeliveryMediums = expandUserDesiredDeliveryMediums(mediums) + } + + if v, ok := d.GetOk("force_alias_creation"); ok { + params.ForceAliasCreation = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("message_action"); ok { + params.MessageAction = aws.String(v.(string)) + } + + if v, ok := d.GetOk("attributes"); ok { + attributes := v.(map[string]interface{}) + params.UserAttributes = expandAttribute(attributes) + } + + if v, ok := d.GetOk("validation_data"); ok { + attributes := v.(map[string]interface{}) + // aws sdk uses the same type for both validation data and user attributes + // https://docs.aws.amazon.com/sdk-for-go/api/service/cognitoidentityprovider/#AdminCreateUserInput + params.ValidationData = expandAttribute(attributes) + } + + if v, ok := d.GetOk("temporary_password"); ok { + params.TemporaryPassword = aws.String(v.(string)) + } + + log.Print("[DEBUG] Creating Cognito User") + + resp, err := conn.AdminCreateUser(params) + if err != nil { + return fmt.Errorf("error creating Cognito User (%s/%s): %w", userPoolId, username, err) + } + + d.SetId(fmt.Sprintf("%s/%s", aws.StringValue(params.UserPoolId), aws.StringValue(resp.User.Username))) + + if v := d.Get("enabled"); !v.(bool) { + disableParams := &cognitoidentityprovider.AdminDisableUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + + _, err := conn.AdminDisableUser(disableParams) + if err != nil { + return fmt.Errorf("error disabling Cognito User (%s): %w", d.Id(), err) + } + } + + if v, ok := d.GetOk("password"); ok { + setPasswordParams := &cognitoidentityprovider.AdminSetUserPasswordInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + Password: aws.String(v.(string)), + Permanent: aws.Bool(true), + } + + _, err := conn.AdminSetUserPassword(setPasswordParams) + if err != nil { + return fmt.Errorf("error setting Cognito User's password (%s): %w", d.Id(), err) + } + } + + return resourceUserRead(d, meta) +} + +func resourceUserRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).CognitoIDPConn + + log.Println("[DEBUG] Reading Cognito User") + + params := &cognitoidentityprovider.AdminGetUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + + user, err := conn.AdminGetUser(params) + if err != nil { + if tfawserr.ErrCodeEquals(err, cognitoidentityprovider.ErrCodeUserNotFoundException) { + log.Printf("[WARN] Cognito User %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading Cognito User (%s): %w", d.Id(), err) + } + + if err := d.Set("attributes", flattenUserAttributes(user.UserAttributes)); err != nil { + return fmt.Errorf("failed setting user attributes (%s): %w", d.Id(), err) + } + + if err := d.Set("mfa_setting_list", user.UserMFASettingList); err != nil { + return fmt.Errorf("failed setting user's mfa settings (%s): %w", d.Id(), err) + } + + d.Set("preferred_mfa_setting", user.PreferredMfaSetting) + d.Set("status", user.UserStatus) + d.Set("enabled", user.Enabled) + d.Set("creation_date", user.UserCreateDate.Format(time.RFC3339)) + d.Set("last_modified_date", user.UserLastModifiedDate.Format(time.RFC3339)) + d.Set("sub", retrieveUserSub(user.UserAttributes)) + + return nil +} + +func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).CognitoIDPConn + + log.Println("[DEBUG] Updating Cognito User") + + if d.HasChange("attributes") { + old, new := d.GetChange("attributes") + + upd, del := computeUserAttributesUpdate(old, new) + + if len(upd) > 0 { + params := &cognitoidentityprovider.AdminUpdateUserAttributesInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + UserAttributes: expandAttribute(upd), + } + + if v, ok := d.GetOk("client_metadata"); ok { + metadata := v.(map[string]interface{}) + params.ClientMetadata = expandUserClientMetadata(metadata) + } + + _, err := conn.AdminUpdateUserAttributes(params) + if err != nil { + return fmt.Errorf("error updating Cognito User Attributes (%s): %w", d.Id(), err) + } + } + if len(del) > 0 { + params := &cognitoidentityprovider.AdminDeleteUserAttributesInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + UserAttributeNames: expandUserAttributesDelete(del), + } + _, err := conn.AdminDeleteUserAttributes(params) + if err != nil { + return fmt.Errorf("error updating Cognito User Attributes (%s): %w", d.Id(), err) + } + } + } + + if d.HasChange("enabled") { + enabled := d.Get("enabled").(bool) + + if enabled { + enableParams := &cognitoidentityprovider.AdminEnableUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + _, err := conn.AdminEnableUser(enableParams) + if err != nil { + return fmt.Errorf("error enabling Cognito User (%s): %w", d.Id(), err) + } + } else { + disableParams := &cognitoidentityprovider.AdminDisableUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + _, err := conn.AdminDisableUser(disableParams) + if err != nil { + return fmt.Errorf("error disabling Cognito User (%s): %w", d.Id(), err) + } + } + } + + if d.HasChange("temporary_password") { + password := d.Get("temporary_password").(string) + + if password != "" { + setPasswordParams := &cognitoidentityprovider.AdminSetUserPasswordInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + Password: aws.String(password), + Permanent: aws.Bool(false), + } + + _, err := conn.AdminSetUserPassword(setPasswordParams) + if err != nil { + return fmt.Errorf("error changing Cognito User's temporary password (%s): %w", d.Id(), err) + } + } else { + d.Set("temporary_password", nil) + } + } + + if d.HasChange("password") { + password := d.Get("password").(string) + + if password != "" { + setPasswordParams := &cognitoidentityprovider.AdminSetUserPasswordInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + Password: aws.String(password), + Permanent: aws.Bool(true), + } + + _, err := conn.AdminSetUserPassword(setPasswordParams) + if err != nil { + return fmt.Errorf("error changing Cognito User's password (%s): %w", d.Id(), err) + } + } else { + d.Set("password", nil) + } + } + + return resourceUserRead(d, meta) +} + +func resourceUserDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).CognitoIDPConn + + log.Print("[DEBUG] Deleting Cognito User") + + params := &cognitoidentityprovider.AdminDeleteUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + + _, err := conn.AdminDeleteUser(params) + if err != nil { + return fmt.Errorf("error deleting Cognito User (%s): %w", d.Id(), err) + } + + return nil +} + +func resourceUserImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + idSplit := strings.Split(d.Id(), "/") + if len(idSplit) != 2 { + return nil, errors.New("error importing Cognito User. Must specify user_pool_id/username") + } + userPoolId := idSplit[0] + name := idSplit[1] + d.Set("user_pool_id", userPoolId) + d.Set("username", name) + return []*schema.ResourceData{d}, nil +} + +func expandAttribute(tfMap map[string]interface{}) []*cognitoidentityprovider.AttributeType { + if len(tfMap) == 0 { + return nil + } + + apiList := make([]*cognitoidentityprovider.AttributeType, 0, len(tfMap)) + + for k, v := range tfMap { + if !UserAttributeKeyMatchesStandardAttribute(k) && !strings.HasPrefix(k, "custom:") { + k = fmt.Sprintf("custom:%v", k) + } + apiList = append(apiList, &cognitoidentityprovider.AttributeType{ + Name: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return apiList +} + +func expandUserAttributesDelete(input []*string) []*string { + result := make([]*string, 0, len(input)) + + for _, v := range input { + if !UserAttributeKeyMatchesStandardAttribute(*v) && !strings.HasPrefix(*v, "custom:") { + formattedV := fmt.Sprintf("custom:%v", *v) + result = append(result, &formattedV) + } else { + result = append(result, v) + } + } + + return result +} + +func flattenUserAttributes(apiList []*cognitoidentityprovider.AttributeType) map[string]interface{} { + tfMap := make(map[string]interface{}) + + for _, apiAttribute := range apiList { + if apiAttribute.Name != nil { + if UserAttributeKeyMatchesStandardAttribute(*apiAttribute.Name) { + tfMap[aws.StringValue(apiAttribute.Name)] = aws.StringValue(apiAttribute.Value) + } else { + name := strings.TrimPrefix(strings.TrimPrefix(aws.StringValue(apiAttribute.Name), "dev:"), "custom:") + tfMap[name] = aws.StringValue(apiAttribute.Value) + } + } + } + + return tfMap +} + +// computeUserAttributesUpdate computes which user attributes should be updated and which ones should be deleted. +// We should do it like this because we cannot set a list of user attributes in cognito. +// We can either perfor update or delete operation +func computeUserAttributesUpdate(old interface{}, new interface{}) (map[string]interface{}, []*string) { + oldMap := old.(map[string]interface{}) + newMap := new.(map[string]interface{}) + + upd := make(map[string]interface{}) + + for k, v := range newMap { + if oldV, ok := oldMap[k]; ok { + if oldV.(string) != v.(string) { + upd[k] = v + } + delete(oldMap, k) + } else { + upd[k] = v + } + } + + del := make([]*string, 0, len(oldMap)) + for k := range oldMap { + del = append(del, &k) + } + + return upd, del +} + +func expandUserDesiredDeliveryMediums(tfSet *schema.Set) []*string { + apiList := []*string{} + + for _, elem := range tfSet.List() { + apiList = append(apiList, aws.String(elem.(string))) + } + + return apiList +} + +func retrieveUserSub(apiList []*cognitoidentityprovider.AttributeType) string { + for _, attr := range apiList { + if aws.StringValue(attr.Name) == "sub" { + return aws.StringValue(attr.Value) + } + } + + return "" +} + +// For ClientMetadata we only need expand since AWS doesn't store its value +func expandUserClientMetadata(tfMap map[string]interface{}) map[string]*string { + apiMap := map[string]*string{} + for k, v := range tfMap { + apiMap[k] = aws.String(v.(string)) + } + + return apiMap +} + +func UserAttributeKeyMatchesStandardAttribute(input string) bool { + if len(input) == 0 { + return false + } + + var standardAttributeKeys = []string{ + "address", + "birthdate", + "email", + "email_verified", + "gender", + "given_name", + "family_name", + "locale", + "middle_name", + "name", + "nickname", + "phone_number", + "phone_number_verified", + "picture", + "preferred_username", + "profile", + "sub", + "updated_at", + "website", + "zoneinfo", + } + + for _, attribute := range standardAttributeKeys { + if input == attribute { + return true + } + } + return false +} diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go new file mode 100644 index 000000000000..1cb9d8199758 --- /dev/null +++ b/internal/service/cognitoidp/user_test.go @@ -0,0 +1,635 @@ +package cognitoidp_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + 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" + "github.com/hashicorp/terraform-provider-aws/internal/service/cognitoidp" +) + +func TestAccCognitoUser_basic(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cognito_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigBasic(rUserPoolName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrSet(resourceName, "sub"), + resource.TestCheckResourceAttr(resourceName, "preferred_mfa_setting", ""), + resource.TestCheckResourceAttr(resourceName, "mfa_setting_list.#", "0"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, + }, + }, + }) +} + +func TestAccCognitoUser_disappears(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cognito_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigBasic(rUserPoolName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, cognitoidp.ResourceUser(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCognitoUser_temporaryPassword(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rClientName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserPassword := sdkacctest.RandString(16) + rUserPasswordUpdated := sdkacctest.RandString(16) + userResourceName := "aws_cognito_user.test" + clientResourceName := "aws_cognito_user_pool_client.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigTemporaryPassword(rUserPoolName, rClientName, rUserName, rUserPassword), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + testAccUserTemporaryPassword(userResourceName, clientResourceName), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), + ), + }, + { + ResourceName: userResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, + }, + { + Config: testAccUserConfigTemporaryPassword(rUserPoolName, rClientName, rUserName, rUserPasswordUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + testAccUserTemporaryPassword(userResourceName, clientResourceName), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), + ), + }, + { + Config: testAccUserConfigNoPassword(rUserPoolName, rClientName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + resource.TestCheckResourceAttr(userResourceName, "temporary_password", ""), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), + ), + }, + }, + }) +} + +func TestAccCognitoUser_password(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rClientName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserPassword := sdkacctest.RandString(16) + rUserPasswordUpdated := sdkacctest.RandString(16) + userResourceName := "aws_cognito_user.test" + clientResourceName := "aws_cognito_user_pool_client.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigPassword(rUserPoolName, rClientName, rUserName, rUserPassword), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + testAccUserPassword(userResourceName, clientResourceName), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeConfirmed), + ), + }, + { + ResourceName: userResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, + }, + { + Config: testAccUserConfigPassword(rUserPoolName, rClientName, rUserName, rUserPasswordUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + testAccUserPassword(userResourceName, clientResourceName), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeConfirmed), + ), + }, + { + Config: testAccUserConfigNoPassword(rUserPoolName, rClientName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(userResourceName), + resource.TestCheckResourceAttr(userResourceName, "password", ""), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeConfirmed), + ), + }, + }, + }) +} + +func TestAccCognitoUser_attributes(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cognito_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigAttributes(rUserPoolName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "attributes.%", "4"), + resource.TestCheckResourceAttr(resourceName, "attributes.one", "1"), + resource.TestCheckResourceAttr(resourceName, "attributes.two", "2"), + resource.TestCheckResourceAttr(resourceName, "attributes.three", "3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, + }, + { + Config: testAccUserConfigAttributesUpdated(rUserPoolName, rUserName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "attributes.%", "4"), + resource.TestCheckResourceAttr(resourceName, "attributes.two", "2"), + resource.TestCheckResourceAttr(resourceName, "attributes.three", "three"), + resource.TestCheckResourceAttr(resourceName, "attributes.four", "4"), + ), + }, + }, + }) +} + +func TestAccCognitoUser_enabled(t *testing.T) { + rUserPoolName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rUserName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cognito_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cognitoidentityprovider.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserConfigEnable(rUserPoolName, rUserName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, + }, + { + Config: testAccUserConfigEnable(rUserPoolName, rUserName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + ), + }, + }, + }) +} + +func testAccCheckUserExists(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) + } + + id := rs.Primary.ID + userName := rs.Primary.Attributes["username"] + userPoolId := rs.Primary.Attributes["user_pool_id"] + + if userName == "" { + return errors.New("No Cognito User Name set") + } + + if userPoolId == "" { + return errors.New("No Cognito User Pool Id set") + } + + if id != fmt.Sprintf("%s/%s", userPoolId, userName) { + return fmt.Errorf(fmt.Sprintf("ID should be user_pool_id/name. ID was %s. name was %s, user_pool_id was %s", id, userName, userPoolId)) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).CognitoIDPConn + + params := &cognitoidentityprovider.AdminGetUserInput{ + Username: aws.String(rs.Primary.Attributes["username"]), + UserPoolId: aws.String(rs.Primary.Attributes["user_pool_id"]), + } + + _, err := conn.AdminGetUser(params) + return err + } +} + +func testAccCheckUserDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).CognitoIDPConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cognito_user" { + continue + } + + params := &cognitoidentityprovider.AdminGetUserInput{ + Username: aws.String(rs.Primary.Attributes["username"]), + UserPoolId: aws.String(rs.Primary.Attributes["user_pool_id"]), + } + + _, err := conn.AdminGetUser(params) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && (awsErr.Code() == cognitoidentityprovider.ErrCodeUserNotFoundException || awsErr.Code() == cognitoidentityprovider.ErrCodeResourceNotFoundException) { + return nil + } + return err + } + } + + return nil +} + +func testAccUserTemporaryPassword(userResName string, clientResName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + userRs, ok := s.RootModule().Resources[userResName] + if !ok { + return fmt.Errorf("Not found: %s", userResName) + } + + clientRs, ok := s.RootModule().Resources[clientResName] + if !ok { + return fmt.Errorf("Not found: %s", clientResName) + } + + userName := userRs.Primary.Attributes["username"] + userPassword := userRs.Primary.Attributes["temporary_password"] + clientId := clientRs.Primary.Attributes["id"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).CognitoIDPConn + + params := &cognitoidentityprovider.InitiateAuthInput{ + AuthFlow: aws.String(cognitoidentityprovider.AuthFlowTypeUserPasswordAuth), + AuthParameters: map[string]*string{ + "USERNAME": aws.String(userName), + "PASSWORD": aws.String(userPassword), + }, + ClientId: aws.String(clientId), + } + + resp, err := conn.InitiateAuth(params) + if err != nil { + return err + } + + if aws.StringValue(resp.ChallengeName) != cognitoidentityprovider.ChallengeNameTypeNewPasswordRequired { + return errors.New("The password is not a temporary password.") + } + + return nil + } +} + +func testAccUserPassword(userResName string, clientResName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + userRs, ok := s.RootModule().Resources[userResName] + if !ok { + return fmt.Errorf("Not found: %s", userResName) + } + + clientRs, ok := s.RootModule().Resources[clientResName] + if !ok { + return fmt.Errorf("Not found: %s", clientResName) + } + + userName := userRs.Primary.Attributes["username"] + userPassword := userRs.Primary.Attributes["password"] + clientId := clientRs.Primary.Attributes["id"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).CognitoIDPConn + + params := &cognitoidentityprovider.InitiateAuthInput{ + AuthFlow: aws.String(cognitoidentityprovider.AuthFlowTypeUserPasswordAuth), + AuthParameters: map[string]*string{ + "USERNAME": aws.String(userName), + "PASSWORD": aws.String(userPassword), + }, + ClientId: aws.String(clientId), + } + + resp, err := conn.InitiateAuth(params) + if err != nil { + return err + } + + if resp.AuthenticationResult == nil { + return errors.New("Authentication has failed.") + } + + return nil + } +} + +func testAccUserConfigBasic(userPoolName string, userName string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q +} +`, userPoolName, userName) +} + +func testAccUserConfigTemporaryPassword(userPoolName string, clientName string, userName string, password string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q + password_policy { + temporary_password_validity_days = 7 + minimum_length = 6 + require_uppercase = false + require_symbols = false + require_numbers = false + } +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[2]q + user_pool_id = aws_cognito_user_pool.test.id + explicit_auth_flows = ["ALLOW_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"] +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q + temporary_password = %[4]q +} +`, userPoolName, clientName, userName, password) +} + +func testAccUserConfigPassword(userPoolName string, clientName string, userName string, password string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q + password_policy { + temporary_password_validity_days = 7 + minimum_length = 6 + require_uppercase = false + require_symbols = false + require_numbers = false + } +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[2]q + user_pool_id = aws_cognito_user_pool.test.id + explicit_auth_flows = ["ALLOW_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"] +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q + password = %[4]q +} +`, userPoolName, clientName, userName, password) +} + +func testAccUserConfigNoPassword(userPoolName string, clientName string, userName string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q + password_policy { + temporary_password_validity_days = 7 + minimum_length = 6 + require_uppercase = false + require_symbols = false + require_numbers = false + } +} + +resource "aws_cognito_user_pool_client" "test" { + name = %[2]q + user_pool_id = aws_cognito_user_pool.test.id + explicit_auth_flows = ["ALLOW_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"] +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q +} +`, userPoolName, clientName, userName) +} + +func testAccUserConfigAttributes(userPoolName string, userName string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q + + schema { + name = "one" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "two" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "three" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "four" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q + + attributes = { + one = "1" + two = "2" + three = "3" + } +} +`, userPoolName, userName) +} + +func testAccUserConfigAttributesUpdated(userPoolName string, userName string) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q + + schema { + name = "one" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "two" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "three" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } + schema { + name = "four" + attribute_data_type = "String" + mutable = true + required = false + developer_only_attribute = false + string_attribute_constraints {} + } +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q + + attributes = { + two = "2" + three = "three" + four = "4" + } +} +`, userPoolName, userName) +} + +func testAccUserConfigEnable(userPoolName string, userName string, enabled bool) string { + return fmt.Sprintf(` +resource "aws_cognito_user_pool" "test" { + name = %[1]q +} + +resource "aws_cognito_user" "test" { + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q + enabled = %[3]t +} +`, userPoolName, userName, enabled) +} diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown new file mode 100644 index 000000000000..4d3b45524359 --- /dev/null +++ b/website/docs/r/cognito_user.html.markdown @@ -0,0 +1,100 @@ +--- +subcategory: "Cognito" +layout: "aws" +page_title: "AWS: aws_cognito_user" +description: |- + Provides a Cognito User resource. +--- + +# Resource: aws_cognito_user + +Provides a Cognito User Resource. + +## Example Usage + +### Basic configuration + +```terraform +resource "aws_cognito_user_pool" "example" { + name = "MyExamplePool" +} + +resource "aws_cognito_user" "example" { + user_pool_id = aws_cognito_user_pool.example.id + username = "example" +} +``` + +### Setting user attributes + +```terraform +resource "aws_cognito_user_pool" "example" { + name = "mypool" + + schema { + name = "terraform" + attribute_data_type = "Boolean" + mutable = false + required = false + developer_only_attribute = false + } + + schema { + name = "foo" + attribute_data_type = "String" + mutable = false + required = false + developer_only_attribute = false + string_attribute_constraints {} + } +} + +resource "aws_cognito_user" "example" { + user_pool_id = aws_cognito_user_pool.example.id + username = "example" + + attributes = { + terraform = true + foo = "bar" + email = "no-reply@hashicorp.com" + email_verified = true + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `user_pool_id` - (Required) The user pool ID for the user pool where the user will be created. +* `user_name` - (Required) The username for the user. Must be unique within the user pool. Must be a UTF-8 string between 1 and 128 characters. After the user is created, the username cannot be changed. + +The following arguments are optional: + +* `attributes` - (Optional) A map that contains user attributes and attribute values to be set for the user. +* `client_metadata` - (Optional) A map of custom key-value pairs that you can provide as input for any custom workflows that user creation triggers. Amazon Cognito does not store the `client_metadata` value. This data is available only to Lambda triggers that are assigned to a user pool to support custom workflows. If your user pool configuration does not include triggers, the ClientMetadata parameter serves no purpose. For more information, see [Customizing User Pool Workflows with Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html). +* `desired_delivery_mediums` - (Optional) A list of mediums to the welcome message will be sent through. Allowed values are `EMAIL` and `SMS`. If it's provided, make sure you have also specified `email` attribute for the `EMAIL` medium and `phone_number` for the `SMS`. More than one value can be specified. Amazon Cognito does not store the `desired_delivery_mediums` value. Defaults to `["SMS"]`. +* `enabled` - (Optional) Specifies whether the user should be enabled after creation. The welcome message will be sent regardless of the `enabled` value. The behavior can be changed with `message_action` argument. Defaults to `true`. +* `force_alias_creation` - (Optional) If this parameter is set to True and the `phone_number` or `email` address specified in the `attributes` parameter already exists as an alias with a different user, Amazon Cognito will migrate the alias from the previous user to the newly created user. The previous user will no longer be able to log in using that alias. Amazon Cognito does not store the `force_alias_creation` value. Defaults to `false`. +* `message_action` - (Optional) Set to `RESEND` to resend the invitation message to a user that already exists and reset the expiration limit on the user's account. Set to `SUPPRESS` to suppress sending the message. Only one value can be specified. Amazon Cognito does not store the `message_action` value. +* `password` - (Optional) The user's permanent password. This password must conform to the password policy specified by user pool the user belongs to. The welcome message always contains only `temporary_password` value. You can suppress sending the welcome message with the `message_action` argument. Amazon Cognito does not store the `password` value. Conflicts with `temporary_password`. +* `temporary_password` - (Optional) The user's temporary password. Conflicts with `password`. +* `validation_data` - (Optional) The user's validation data. This is an array of name-value pairs that contain user attributes and attribute values that you can use for custom validation, such as restricting the types of user accounts that can be registered. Amazon Cognito does not store the `validation_data` value. For more information, see [Customizing User Pool Workflows with Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html). + +~> **NOTE:** Clearing `password` or `temporary_password` does not reset user's password in Cognito. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `status` - current user status. +* `sub` - unique user id that is never reassignable to another user. +* `mfa_preference` - user's settings regarding MFA settings and preferences. + +## Import + +Cognito User can be imported using the `user_pool_id`/`name` attributes concatenated, e.g., + +``` +$ terraform import aws_cognito_user.user us-east-1_vG78M4goG/user +```