From a76b8d433bdd5c906be588f376c16021856a8e12 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 22 Jun 2021 16:45:19 +0300 Subject: [PATCH 01/36] Add aws_cognito_user Only user-attributes are currently supported --- aws/provider.go | 1 + aws/resource_aws_cognito_user.go | 284 +++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 aws/resource_aws_cognito_user.go diff --git a/aws/provider.go b/aws/provider.go index 09bb2c0de5d4..df9e4981453d 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -578,6 +578,7 @@ func Provider() *schema.Provider { "aws_cognito_identity_pool_roles_attachment": resourceAwsCognitoIdentityPoolRolesAttachment(), "aws_cognito_identity_provider": resourceAwsCognitoIdentityProvider(), "aws_cognito_resource_server": resourceAwsCognitoResourceServer(), + "aws_cognito_user": resourceAwsCognitoUser(), "aws_cognito_user_group": resourceAwsCognitoUserGroup(), "aws_cognito_user_pool": resourceAwsCognitoUserPool(), "aws_cognito_user_pool_client": resourceAwsCognitoUserPoolClient(), diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go new file mode 100644 index 000000000000..0ed2acb668ca --- /dev/null +++ b/aws/resource_aws_cognito_user.go @@ -0,0 +1,284 @@ +package aws + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAwsCognitoUser() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCognitoUserCreate, + Read: resourceAwsCognitoUserRead, + Update: resourceAwsCognitoUserUpdate, + Delete: resourceAwsCognitoUserDelete, + + Importer: &schema.ResourceImporter{ + State: resourceAwsCognitoUserImport, + }, + + // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html + Schema: map[string]*schema.Schema{ + "user_attribute": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + // Sensitive: true, + }, + }, + }, + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "user_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + params := &cognitoidentityprovider.AdminCreateUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + + if v, ok := d.GetOk("user_attribute"); ok { + attributes := v.(*schema.Set) + params.UserAttributes = expandCognitoUserAttributes(attributes) + } + + log.Print("[DEBUG] Creating Cognito User") + + resp, err := conn.AdminCreateUser(params) + if err != nil { + return fmt.Errorf("Error creating Cognito User: %s", err) + } + + d.SetId(fmt.Sprintf("%s/%s", *params.UserPoolId, *resp.User.Username)) + + return resourceAwsCognitoUserRead(d, meta) +} + +func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + log.Println("[DEBUG] Creating request struct") + params := &cognitoidentityprovider.AdminGetUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + log.Println("[DEBUG] Request input: ", params) + log.Println("[DEBUG] Reading Cognito User") + + user, err := conn.AdminGetUser(params) + if err != nil { + log.Println("[ERROR] Error reading Cognito User: ", err) + if isAWSErr(err, "ResourceNotFoundException", "") { + log.Printf("[WARN] Cognito User %s is already gone", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Cognito User: %s", err) + } + + if err := d.Set("user_attribute", flattenCognitoUserAttributes(user.UserAttributes)); err != nil { + return fmt.Errorf("failed setting user_attributes: %w", err) + } + + return nil +} + +func resourceAwsCognitoUserUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + log.Println("[DEBUG] Updating Cognito User") + + if d.HasChange("user_attribute") { + o, n := d.GetChange("user_attribute") + + upd, del := computeCognitoUserAttributesUpdate(o, n) + + if upd.Len() > 0 { + params := &cognitoidentityprovider.AdminUpdateUserAttributesInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + UserAttributes: expandCognitoUserAttributes(upd), + } + _, err := conn.AdminUpdateUserAttributes(params) + if err != nil { + if isAWSErr(err, "ResourceNotFoundException", "") { + log.Printf("[WARN] Cognito User %s is already gone", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error updating Cognito User Attributes: %s", 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: del, + } + _, err := conn.AdminDeleteUserAttributes(params) + if err != nil { + if isAWSErr(err, "ResourceNotFoundException", "") { + log.Printf("[WARN] Cognito User %s is already gone", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error updating Cognito User Attributes: %s", err) + } + } + } + + return resourceAwsCognitoUserRead(d, meta) +} + +func resourceAwsCognitoUserDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cognitoidpconn + + params := &cognitoidentityprovider.AdminDeleteUserInput{ + Username: aws.String(d.Get("username").(string)), + UserPoolId: aws.String(d.Get("user_pool_id").(string)), + } + + log.Print("[DEBUG] Deleting Cognito User") + + _, err := conn.AdminDeleteUser(params) + if err != nil { + return fmt.Errorf("Error deleting Cognito User: %s", err) + } + + return nil +} + +func resourceAwsCognitoUserImport(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 expandCognitoUserAttributes(tfSet *schema.Set) []*cognitoidentityprovider.AttributeType { + if tfSet.Len() == 0 { + return nil + } + + apiList := make([]*cognitoidentityprovider.AttributeType, 0, tfSet.Len()) + + for _, tfAttribute := range tfSet.List() { + apiAttribute := tfAttribute.(map[string]interface{}) + apiList = append(apiList, &cognitoidentityprovider.AttributeType{ + Name: aws.String(apiAttribute["name"].(string)), + Value: aws.String(apiAttribute["value"].(string)), + }) + } + + return apiList +} + +func flattenCognitoUserAttributes(apiList []*cognitoidentityprovider.AttributeType) *schema.Set { + if len(apiList) == 1 { + return nil + } + + tfList := []interface{}{} + + for _, apiAttribute := range apiList { + // not sure if this is the best way to deal with this system attrubute + if *apiAttribute.Name == "sub" { + continue + } + + tfAttribute := map[string]interface{}{} + + if apiAttribute.Name != nil { + tfAttribute["name"] = aws.StringValue(apiAttribute.Name) + } + + if apiAttribute.Value != nil { + tfAttribute["value"] = aws.StringValue(apiAttribute.Value) + } + + tfList = append(tfList, tfAttribute) + } + + tfSet := schema.NewSet(cognitoUserAttributeHash, tfList) + + return tfSet +} + +// computeCognitoUserAttributesUpdate computes which userattributes should be updated and which ones should be deleted. +// We should do it like this because we cannot explicitly set a list of user attributes in cognito. We can either perform +// an update or delete operation. +func computeCognitoUserAttributesUpdate(old interface{}, new interface{}) (*schema.Set, []*string) { + oldMap := map[string]interface{}{} + + oldList := old.(*schema.Set).List() + newList := new.(*schema.Set).List() + + upd := schema.NewSet(cognitoUserAttributeHash, []interface{}{}) + del := []*string{} + + for _, v := range oldList { + vMap := v.(map[string]interface{}) + oldMap[vMap["name"].(string)] = vMap["value"] + } + + for _, v := range newList { + vMap := v.(map[string]interface{}) + if oldV, ok := oldMap[vMap["name"].(string)]; ok { + if oldV != vMap["value"] { + upd.Add(map[string]interface{}{ + "name": vMap["name"].(string), + "value": vMap["value"], + }) + } + delete(oldMap, vMap["name"].(string)) + } else { + upd.Add(map[string]interface{}{ + "name": vMap["name"].(string), + "value": vMap["value"], + }) + } + } + + for k := range oldMap { + del = append(del, &k) + } + + return upd, del +} + +func cognitoUserAttributeHash(attr interface{}) int { + attrMap := attr.(map[string]interface{}) + + return schema.HashString(attrMap["name"]) +} From 35407fb77faacfaf0a6bddbb6357673f9098c7d2 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 22 Jun 2021 17:22:05 +0300 Subject: [PATCH 02/36] make attribute value sensitive --- aws/resource_aws_cognito_user.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index 0ed2acb668ca..a676a3564609 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -34,9 +34,9 @@ func resourceAwsCognitoUser() *schema.Resource { Required: true, }, "value": { - Type: schema.TypeString, - Optional: true, - // Sensitive: true, + Type: schema.TypeString, + Optional: true, + Sensitive: true, }, }, }, From cbd139357a92a994adbf0e4dc95b01e277cb0286 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Sat, 14 Aug 2021 13:40:18 +0300 Subject: [PATCH 03/36] add desiredDeliveryMediums and enabled --- aws/resource_aws_cognito_user.go | 102 +++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index a676a3564609..935eaef34410 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceAwsCognitoUser() *schema.Resource { @@ -24,9 +25,23 @@ func resourceAwsCognitoUser() *schema.Resource { // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html Schema: map[string]*schema.Schema{ - "user_attribute": { - Type: schema.TypeSet, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "user_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "enabled": { + Type: schema.TypeBool, Optional: true, + Default: true, + }, + "user_attribute": { + Type: schema.TypeSet, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -40,16 +55,18 @@ func resourceAwsCognitoUser() *schema.Resource { }, }, }, + Optional: true, }, - "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "user_pool_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "desired_delivery_mediums": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + cognitoidentityprovider.DeliveryMediumTypeSms, + cognitoidentityprovider.DeliveryMediumTypeEmail, + }, false), + }, + Optional: true, }, }, } @@ -68,6 +85,11 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro params.UserAttributes = expandCognitoUserAttributes(attributes) } + if v, ok := d.GetOk("desired_delivery_mediums"); ok { + mediums := v.(*schema.Set) + params.DesiredDeliveryMediums = expandDesiredDeliveryMediums(mediums) + } + log.Print("[DEBUG] Creating Cognito User") resp, err := conn.AdminCreateUser(params) @@ -77,6 +99,19 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro d.SetId(fmt.Sprintf("%s/%s", *params.UserPoolId, *resp.User.Username)) + if v := d.Get("enabled"); !v.(bool) { + log.Println("[DEBUG] the user enabled value is ", v) + 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", err) + } + } + return resourceAwsCognitoUserRead(d, meta) } @@ -115,9 +150,9 @@ func resourceAwsCognitoUserUpdate(d *schema.ResourceData, meta interface{}) erro log.Println("[DEBUG] Updating Cognito User") if d.HasChange("user_attribute") { - o, n := d.GetChange("user_attribute") + old, new := d.GetChange("user_attribute") - upd, del := computeCognitoUserAttributesUpdate(o, n) + upd, del := computeCognitoUserAttributesUpdate(old, new) if upd.Len() > 0 { params := &cognitoidentityprovider.AdminUpdateUserAttributesInput{ @@ -153,6 +188,30 @@ func resourceAwsCognitoUserUpdate(d *schema.ResourceData, meta interface{}) erro } } + 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", 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", err) + } + } + } + return resourceAwsCognitoUserRead(d, meta) } @@ -212,7 +271,6 @@ func flattenCognitoUserAttributes(apiList []*cognitoidentityprovider.AttributeTy tfList := []interface{}{} for _, apiAttribute := range apiList { - // not sure if this is the best way to deal with this system attrubute if *apiAttribute.Name == "sub" { continue } @@ -235,9 +293,19 @@ func flattenCognitoUserAttributes(apiList []*cognitoidentityprovider.AttributeTy return tfSet } -// computeCognitoUserAttributesUpdate computes which userattributes should be updated and which ones should be deleted. -// We should do it like this because we cannot explicitly set a list of user attributes in cognito. We can either perform -// an update or delete operation. +func expandDesiredDeliveryMediums(tfSet *schema.Set) []*string { + apiList := []*string{} + + for _, elem := range tfSet.List() { + apiList = append(apiList, aws.String(elem.(string))) + } + + return apiList +} + +// computeCognitoUserAttributesUpdate 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 man update +// or delete operation. func computeCognitoUserAttributesUpdate(old interface{}, new interface{}) (*schema.Set, []*string) { oldMap := map[string]interface{}{} From d9a7743a0e0ae45988e2c4778cebadfe934ac2ce Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 12 Oct 2021 22:38:18 +0300 Subject: [PATCH 04/36] Add client_metadata, force_alias_creation and message_action --- aws/resource_aws_cognito_user.go | 86 ++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index 935eaef34410..b8269b10f423 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -25,21 +25,39 @@ func resourceAwsCognitoUser() *schema.Resource { // https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html Schema: map[string]*schema.Schema{ - "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "client_metadata": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, }, - "user_pool_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "desired_delivery_mediums": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + cognitoidentityprovider.DeliveryMediumTypeSms, + cognitoidentityprovider.DeliveryMediumTypeEmail, + }, false), + }, + Optional: true, }, "enabled": { Type: schema.TypeBool, Optional: true, Default: true, }, + "force_alias_creation": { + Type: schema.TypeBool, + Optional: true, + }, + "message_action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + cognitoidentityprovider.MessageActionTypeResend, + cognitoidentityprovider.MessageActionTypeSuppress, + }, false), + }, "user_attribute": { Type: schema.TypeSet, Elem: &schema.Resource{ @@ -50,23 +68,22 @@ func resourceAwsCognitoUser() *schema.Resource { }, "value": { Type: schema.TypeString, - Optional: true, + Required: true, Sensitive: true, }, }, }, Optional: true, }, - "desired_delivery_mediums": { - Type: schema.TypeSet, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.StringInSlice([]string{ - cognitoidentityprovider.DeliveryMediumTypeSms, - cognitoidentityprovider.DeliveryMediumTypeEmail, - }, false), - }, - Optional: true, + "user_pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, }, }, } @@ -87,13 +104,30 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro if v, ok := d.GetOk("desired_delivery_mediums"); ok { mediums := v.(*schema.Set) - params.DesiredDeliveryMediums = expandDesiredDeliveryMediums(mediums) + params.DesiredDeliveryMediums = expandCognitoUserDesiredDeliveryMediums(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("client_metadata"); ok { + metadata := v.(map[string]interface{}) + params.ClientMetadata = expandCognitoUserClientMetadata(metadata) } log.Print("[DEBUG] Creating Cognito User") resp, err := conn.AdminCreateUser(params) if err != nil { + if isAWSErr(err, "AliasExistsException", "") { + log.Println("[ERROR] User alias already exists. To override the alias set `force_alias_creation` attribute to `true`.") + return nil + } return fmt.Errorf("Error creating Cognito User: %s", err) } @@ -293,7 +327,7 @@ func flattenCognitoUserAttributes(apiList []*cognitoidentityprovider.AttributeTy return tfSet } -func expandDesiredDeliveryMediums(tfSet *schema.Set) []*string { +func expandCognitoUserDesiredDeliveryMediums(tfSet *schema.Set) []*string { apiList := []*string{} for _, elem := range tfSet.List() { @@ -345,6 +379,16 @@ func computeCognitoUserAttributesUpdate(old interface{}, new interface{}) (*sche return upd, del } +// For ClientMetadata we only need expand since AWS doesn't store its value +func expandCognitoUserClientMetadata(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 cognitoUserAttributeHash(attr interface{}) int { attrMap := attr.(map[string]interface{}) From 9b3abf49cc15e74ff40e68db00e66c85dd3cc1ca Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 12 Oct 2021 23:12:19 +0300 Subject: [PATCH 05/36] add validation data --- aws/resource_aws_cognito_user.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index b8269b10f423..beb5b09fc1bf 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -85,6 +85,23 @@ func resourceAwsCognitoUser() *schema.Resource { Required: true, ForceNew: true, }, + "validation_data": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + }, + }, + Optional: true, + }, }, } } @@ -120,6 +137,13 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro params.ClientMetadata = expandCognitoUserClientMetadata(metadata) } + if v, ok := d.GetOk("validation_data"); ok { + attributes := v.(*schema.Set) + // 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 = expandCognitoUserAttributes(attributes) + } + log.Print("[DEBUG] Creating Cognito User") resp, err := conn.AdminCreateUser(params) From 7284b8a0d128282b68f2b8856ef582efde663574 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 12 Oct 2021 23:18:13 +0300 Subject: [PATCH 06/36] Add temporary and permanent passwords --- aws/resource_aws_cognito_user.go | 86 +++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index beb5b09fc1bf..cc934d31a6e1 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -85,6 +85,20 @@ func resourceAwsCognitoUser() *schema.Resource { Required: true, ForceNew: 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.TypeSet, Elem: &schema.Resource{ @@ -109,14 +123,16 @@ func resourceAwsCognitoUser() *schema.Resource { func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cognitoidpconn + log.Print("[DEBUG] Creating Cognito User") + params := &cognitoidentityprovider.AdminCreateUserInput{ Username: aws.String(d.Get("username").(string)), UserPoolId: aws.String(d.Get("user_pool_id").(string)), } - if v, ok := d.GetOk("user_attribute"); ok { - attributes := v.(*schema.Set) - params.UserAttributes = expandCognitoUserAttributes(attributes) + if v, ok := d.GetOk("client_metadata"); ok { + metadata := v.(map[string]interface{}) + params.ClientMetadata = expandCognitoUserClientMetadata(metadata) } if v, ok := d.GetOk("desired_delivery_mediums"); ok { @@ -132,9 +148,9 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro params.MessageAction = aws.String(v.(string)) } - if v, ok := d.GetOk("client_metadata"); ok { - metadata := v.(map[string]interface{}) - params.ClientMetadata = expandCognitoUserClientMetadata(metadata) + if v, ok := d.GetOk("user_attribute"); ok { + attributes := v.(*schema.Set) + params.UserAttributes = expandCognitoUserAttributes(attributes) } if v, ok := d.GetOk("validation_data"); ok { @@ -144,7 +160,9 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro params.ValidationData = expandCognitoUserAttributes(attributes) } - log.Print("[DEBUG] Creating Cognito User") + if v, ok := d.GetOk("temporary_password"); ok { + params.TemporaryPassword = aws.String(v.(string)) + } resp, err := conn.AdminCreateUser(params) if err != nil { @@ -158,7 +176,6 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro d.SetId(fmt.Sprintf("%s/%s", *params.UserPoolId, *resp.User.Username)) if v := d.Get("enabled"); !v.(bool) { - log.Println("[DEBUG] the user enabled value is ", v) disableParams := &cognitoidentityprovider.AdminDisableUserInput{ Username: aws.String(d.Get("username").(string)), UserPoolId: aws.String(d.Get("user_pool_id").(string)), @@ -170,19 +187,32 @@ func resourceAwsCognitoUserCreate(d *schema.ResourceData, meta interface{}) erro } } + 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", err) + } + } + return resourceAwsCognitoUserRead(d, meta) } func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).cognitoidpconn - log.Println("[DEBUG] Creating request struct") + 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)), } - log.Println("[DEBUG] Request input: ", params) - log.Println("[DEBUG] Reading Cognito User") user, err := conn.AdminGetUser(params) if err != nil { @@ -270,19 +300,49 @@ func resourceAwsCognitoUserUpdate(d *schema.ResourceData, meta interface{}) erro } } + if d.HasChange("temporary_password") || d.HasChange("password") { + tempPassword := d.Get("temporary_password").(string) + permanentPassword := d.Get("password").(string) + + var password string + var isPermanent bool + + // both passwords cannot be non-empty because of ConflictsWith + if tempPassword != "" { + password = tempPassword + } else if permanentPassword != "" { + password = permanentPassword + isPermanent = true + } + + if password != "" { + tempPasswordParams := &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(isPermanent), + } + + _, err := conn.AdminSetUserPassword(tempPasswordParams) + if err != nil { + return fmt.Errorf("Error changing Cognito User's password: %s", err) + } + } + } + return resourceAwsCognitoUserRead(d, meta) } func resourceAwsCognitoUserDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*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)), } - log.Print("[DEBUG] Deleting Cognito User") - _, err := conn.AdminDeleteUser(params) if err != nil { return fmt.Errorf("Error deleting Cognito User: %s", err) From 6ee25bfb9b2fbf50eabff057757348f4a71cbb94 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 13 Oct 2021 23:03:42 +0300 Subject: [PATCH 07/36] logging fixes; add user status computed field --- aws/resource_aws_cognito_user.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index cc934d31a6e1..118e2bf3bcd0 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -85,6 +85,10 @@ func resourceAwsCognitoUser() *schema.Resource { Required: true, ForceNew: true, }, + "user_status": { + Type: schema.TypeString, + Computed: true, + }, "password": { Type: schema.TypeString, Sensitive: true, @@ -216,9 +220,8 @@ func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error user, err := conn.AdminGetUser(params) if err != nil { - log.Println("[ERROR] Error reading Cognito User: ", err) if isAWSErr(err, "ResourceNotFoundException", "") { - log.Printf("[WARN] Cognito User %s is already gone", d.Id()) + log.Printf("[WARN] Cognito User %s not found, removing from state", d.Id()) d.SetId("") return nil } @@ -229,6 +232,10 @@ func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("failed setting user_attributes: %w", err) } + if err := d.Set("user_status", user.UserStatus); err != nil { + return fmt.Errorf("failed setting user_status: %w", err) + } + return nil } From cc1dac11a3ada693891d9617bb6b4dda47b37d17 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 20 Oct 2021 19:23:47 +0300 Subject: [PATCH 08/36] add creation and last_modified dates --- aws/resource_aws_cognito_user.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index 118e2bf3bcd0..6b3613e471e8 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -30,6 +30,10 @@ func resourceAwsCognitoUser() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, }, + "creation_date": { + Type: schema.TypeString, + Computed: true, + }, "desired_delivery_mediums": { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -50,6 +54,10 @@ func resourceAwsCognitoUser() *schema.Resource { Type: schema.TypeBool, Optional: true, }, + "last_modified_date": { + Type: schema.TypeString, + Computed: true, + }, "message_action": { Type: schema.TypeString, Optional: true, @@ -236,6 +244,13 @@ func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("failed setting user_status: %w", err) } + if err := d.Set("creation_date", user.UserCreateDate.Format(time.RFC3339)); err != nil { + return fmt.Errorf("failed setting user's creation_date: %w", err) + } + if err := d.Set("last_modified_date", user.UserLastModifiedDate.Format(time.RFC3339)); err != nil { + return fmt.Errorf("failed setting user's last_modified_date: %w", err) + } + return nil } From e6183c390fd2a410832409136fb45937a18a446f Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 20 Oct 2021 19:24:56 +0300 Subject: [PATCH 09/36] rename user_status to status --- aws/resource_aws_cognito_user.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws/resource_aws_cognito_user.go b/aws/resource_aws_cognito_user.go index 6b3613e471e8..6f01fdf6805c 100644 --- a/aws/resource_aws_cognito_user.go +++ b/aws/resource_aws_cognito_user.go @@ -93,7 +93,7 @@ func resourceAwsCognitoUser() *schema.Resource { Required: true, ForceNew: true, }, - "user_status": { + "status": { Type: schema.TypeString, Computed: true, }, @@ -240,7 +240,7 @@ func resourceAwsCognitoUserRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("failed setting user_attributes: %w", err) } - if err := d.Set("user_status", user.UserStatus); err != nil { + if err := d.Set("status", user.UserStatus); err != nil { return fmt.Errorf("failed setting user_status: %w", err) } From cc89f04455aa007dbed0f5230ca01fa350112c41 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 12 Nov 2021 18:16:45 +0200 Subject: [PATCH 10/36] add computed mfa_preference --- internal/service/cognitoidp/user.go | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 5c6e4e65c68c..5c38829ffe35 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -69,6 +69,26 @@ func ResourceUser() *schema.Resource { cognitoidentityprovider.MessageActionTypeSuppress, }, false), }, + "mfa_preference": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sms_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "software_token_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "preferred_mfa": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Computed: true, + }, "user_attribute": { Type: schema.TypeSet, Elem: &schema.Resource{ @@ -254,6 +274,10 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("failed setting user's last_modified_date: %w", err) } + if err := d.Set("mfa_preference", flattenUserMfaPreference(user.MFAOptions, user.UserMFASettingList, user.PreferredMfaSetting)); err != nil { + return fmt.Errorf("failed settings user's mfa_preference: %w", err) + } + return nil } @@ -498,6 +522,31 @@ func expandUserClientMetadata(tfMap map[string]interface{}) map[string]*string { return apiMap } +func flattenUserMfaPreference(mfaOptions []*cognitoidentityprovider.MFAOptionType, mfaSettingsList []*string, preferredMfa *string) []interface{} { + preference := map[string]interface{}{} + + for _, setting := range mfaSettingsList { + v := aws.StringValue(setting) + + if v == cognitoidentityprovider.ChallengeNameTypeSmsMfa { + preference["sms_enabled"] = true + } else if v == cognitoidentityprovider.ChallengeNameTypeSoftwareTokenMfa { + preference["software_token_enabled"] = true + } + } + + if len(mfaOptions) > 0 { + // mfaOptions.DeliveryMediums can only have value SMS so we check only first element + if aws.StringValue(mfaOptions[0].DeliveryMedium) == cognitoidentityprovider.DeliveryMediumTypeSms { + preference["sms_enabled"] = true + } + } + + preference["preferred_mfa"] = aws.StringValue(preferredMfa) + + return []interface{}{preference} +} + func userAttributeHash(attr interface{}) int { attrMap := attr.(map[string]interface{}) From 4da375ab07937681e1222ec8563d02e54292e8df Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Mon, 15 Nov 2021 14:35:43 +0200 Subject: [PATCH 11/36] make user_attributes typeMap --- internal/service/cognitoidp/user.go | 215 +++++++++++++++------------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 5c38829ffe35..7960ecd606e9 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -89,20 +89,10 @@ func ResourceUser() *schema.Resource { }, Computed: true, }, - "user_attribute": { - Type: schema.TypeSet, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "value": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - }, + "attributes": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, }, Optional: true, }, @@ -135,19 +125,9 @@ func ResourceUser() *schema.Resource { ConflictsWith: []string{"password"}, }, "validation_data": { - Type: schema.TypeSet, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "value": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - }, - }, + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, }, Optional: true, }, @@ -183,13 +163,13 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { params.MessageAction = aws.String(v.(string)) } - if v, ok := d.GetOk("user_attribute"); ok { - attributes := v.(*schema.Set) + if v, ok := d.GetOk("attributes"); ok { + attributes := v.(map[string]interface{}) params.UserAttributes = expandUserAttributes(attributes) } if v, ok := d.GetOk("validation_data"); ok { - attributes := v.(*schema.Set) + 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 = expandUserAttributes(attributes) @@ -259,8 +239,8 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error reading Cognito User: %s", err) } - if err := d.Set("user_attribute", flattenUserAttributes(user.UserAttributes)); err != nil { - return fmt.Errorf("failed setting user_attributes: %w", err) + if err := d.Set("attributes", flattenUserAttributes(user.UserAttributes)); err != nil { + return fmt.Errorf("failed setting attributes: %w", err) } if err := d.Set("status", user.UserStatus); err != nil { @@ -286,12 +266,12 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { log.Println("[DEBUG] Updating Cognito User") - if d.HasChange("user_attribute") { - old, new := d.GetChange("user_attribute") + if d.HasChange("attributes") { + old, new := d.GetChange("attributes") upd, del := computeUserAttributesUpdate(old, new) - if upd.Len() > 0 { + if len(upd) > 0 { params := &cognitoidentityprovider.AdminUpdateUserAttributesInput{ Username: aws.String(d.Get("username").(string)), UserPoolId: aws.String(d.Get("user_pool_id").(string)), @@ -311,7 +291,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { params := &cognitoidentityprovider.AdminDeleteUserAttributesInput{ Username: aws.String(d.Get("username").(string)), UserPoolId: aws.String(d.Get("user_pool_id").(string)), - UserAttributeNames: del, + UserAttributeNames: expandUserAttributesDelete(del), } _, err := conn.AdminDeleteUserAttributes(params) if err != nil { @@ -412,99 +392,84 @@ func resourceUserImport(d *schema.ResourceData, meta interface{}) ([]*schema.Res return []*schema.ResourceData{d}, nil } -func expandUserAttributes(tfSet *schema.Set) []*cognitoidentityprovider.AttributeType { - if tfSet.Len() == 0 { +func expandUserAttributes(tfMap map[string]interface{}) []*cognitoidentityprovider.AttributeType { + if len(tfMap) == 0 { return nil } - apiList := make([]*cognitoidentityprovider.AttributeType, 0, tfSet.Len()) + apiList := make([]*cognitoidentityprovider.AttributeType, 0, len(tfMap)) - for _, tfAttribute := range tfSet.List() { - apiAttribute := tfAttribute.(map[string]interface{}) + 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(apiAttribute["name"].(string)), - Value: aws.String(apiAttribute["value"].(string)), + Name: aws.String(k), + Value: aws.String(v.(string)), }) } return apiList } -func flattenUserAttributes(apiList []*cognitoidentityprovider.AttributeType) *schema.Set { - if len(apiList) == 1 { - return nil - } - - tfList := []interface{}{} - - for _, apiAttribute := range apiList { - if *apiAttribute.Name == "sub" { - continue - } - - tfAttribute := map[string]interface{}{} - - if apiAttribute.Name != nil { - tfAttribute["name"] = aws.StringValue(apiAttribute.Name) - } +func expandUserAttributesDelete(input []*string) []*string { + result := make([]*string, 0, len(input)) - if apiAttribute.Value != nil { - tfAttribute["value"] = aws.StringValue(apiAttribute.Value) + 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) } - - tfList = append(tfList, tfAttribute) } - tfSet := schema.NewSet(userAttributeHash, tfList) - - return tfSet + return result } -func expandUserDesiredDeliveryMediums(tfSet *schema.Set) []*string { - apiList := []*string{} - - for _, elem := range tfSet.List() { - apiList = append(apiList, aws.String(elem.(string))) +func flattenUserAttributes(apiList []*cognitoidentityprovider.AttributeType) map[string]interface{} { + tfMap := make(map[string]interface{}) + + if len(apiList) > 1 { + for _, apiAttribute := range apiList { + if apiAttribute.Name != nil { + if UserAttributeKeyMatchesStandardAttribute(*apiAttribute.Name) { + if aws.StringValue(apiAttribute.Name) == "sub" { + continue + } + 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 apiList + 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 man update -// or delete operation. -func computeUserAttributesUpdate(old interface{}, new interface{}) (*schema.Set, []*string) { - oldMap := map[string]interface{}{} - - oldList := old.(*schema.Set).List() - newList := new.(*schema.Set).List() - - upd := schema.NewSet(userAttributeHash, []interface{}{}) - del := []*string{} - - for _, v := range oldList { - vMap := v.(map[string]interface{}) - oldMap[vMap["name"].(string)] = vMap["value"] - } - - for _, v := range newList { - vMap := v.(map[string]interface{}) - if oldV, ok := oldMap[vMap["name"].(string)]; ok { - if oldV != vMap["value"] { - upd.Add(map[string]interface{}{ - "name": vMap["name"].(string), - "value": vMap["value"], - }) +// 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, vMap["name"].(string)) + delete(oldMap, k) } else { - upd.Add(map[string]interface{}{ - "name": vMap["name"].(string), - "value": vMap["value"], - }) + upd[k] = v } } + del := make([]*string, 0, len(oldMap)) for k := range oldMap { del = append(del, &k) } @@ -512,6 +477,16 @@ func computeUserAttributesUpdate(old interface{}, new interface{}) (*schema.Set, 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 +} + // 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{} @@ -552,3 +527,39 @@ func userAttributeHash(attr interface{}) int { return schema.HashString(attrMap["name"]) } + +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 +} From 730c494077395d5a3c32b349112e312c4a5905ec Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Mon, 15 Nov 2021 17:06:19 +0200 Subject: [PATCH 12/36] add computed sub --- internal/service/cognitoidp/user.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 7960ecd606e9..676e39ed65a1 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -110,6 +110,10 @@ func ResourceUser() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "sub": { + Type: schema.TypeString, + Computed: true, + }, "password": { Type: schema.TypeString, Sensitive: true, @@ -247,6 +251,10 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("failed setting user_status: %w", err) } + if err := d.Set("sub", flattenUserSub(user.UserAttributes)); err != nil { + return fmt.Errorf("failed setting user's sub: %w", err) + } + if err := d.Set("creation_date", user.UserCreateDate.Format(time.RFC3339)); err != nil { return fmt.Errorf("failed setting user's creation_date: %w", err) } @@ -487,6 +495,16 @@ func expandUserDesiredDeliveryMediums(tfSet *schema.Set) []*string { return apiList } +func flattenUserSub(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{} From 930a0f120fa6e336f9306bb9fece945396625b0a Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Mon, 15 Nov 2021 17:06:27 +0200 Subject: [PATCH 13/36] reorganize schema --- internal/service/cognitoidp/user.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 676e39ed65a1..1fc09f2afcdf 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -28,6 +28,13 @@ func ResourceUser() *schema.Resource { // 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, + }, + Optional: true, + }, "client_metadata": { Type: schema.TypeMap, Elem: &schema.Schema{Type: schema.TypeString}, @@ -89,13 +96,6 @@ func ResourceUser() *schema.Resource { }, Computed: true, }, - "attributes": { - Type: schema.TypeMap, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - }, "user_pool_id": { Type: schema.TypeString, Required: true, @@ -541,9 +541,7 @@ func flattenUserMfaPreference(mfaOptions []*cognitoidentityprovider.MFAOptionTyp } func userAttributeHash(attr interface{}) int { - attrMap := attr.(map[string]interface{}) - - return schema.HashString(attrMap["name"]) + return schema.HashString(attr.(map[string]interface{})["name"]) } func UserAttributeKeyMatchesStandardAttribute(input string) bool { From f12216e08262306f267b1ddd0ddf57826ada290d Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 23 Nov 2021 12:50:27 +0200 Subject: [PATCH 14/36] basic and dissapears tests and fixes --- internal/service/cognitoidp/user.go | 48 +++++--- internal/service/cognitoidp/user_test.go | 147 +++++++++++++++++++++++ 2 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 internal/service/cognitoidp/user_test.go diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 1fc09f2afcdf..214b53581bd3 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -235,7 +235,7 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { user, err := conn.AdminGetUser(params) if err != nil { - if tfawserr.ErrMessageContains(err, "ResourceNotFoundException", "") { + if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { log.Printf("[WARN] Cognito User %s not found, removing from state", d.Id()) d.SetId("") return nil @@ -244,26 +244,31 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { } if err := d.Set("attributes", flattenUserAttributes(user.UserAttributes)); err != nil { - return fmt.Errorf("failed setting attributes: %w", err) + return fmt.Errorf("failed setting user attributes: %w", err) } if err := d.Set("status", user.UserStatus); err != nil { - return fmt.Errorf("failed setting user_status: %w", err) + return fmt.Errorf("failed setting user status: %w", err) } if err := d.Set("sub", flattenUserSub(user.UserAttributes)); err != nil { - return fmt.Errorf("failed setting user's sub: %w", err) + return fmt.Errorf("failed setting user sub: %w", err) + } + + if err := d.Set("enabled", user.Enabled); err != nil { + return fmt.Errorf("failed setting user enabled status: %w", err) } if err := d.Set("creation_date", user.UserCreateDate.Format(time.RFC3339)); err != nil { - return fmt.Errorf("failed setting user's creation_date: %w", err) + return fmt.Errorf("failed setting user creation_date: %w", err) } + if err := d.Set("last_modified_date", user.UserLastModifiedDate.Format(time.RFC3339)); err != nil { - return fmt.Errorf("failed setting user's last_modified_date: %w", err) + return fmt.Errorf("failed setting user last_modified_date: %w", err) } if err := d.Set("mfa_preference", flattenUserMfaPreference(user.MFAOptions, user.UserMFASettingList, user.PreferredMfaSetting)); err != nil { - return fmt.Errorf("failed settings user's mfa_preference: %w", err) + return fmt.Errorf("failed settings user mfa_preference: %w", err) } return nil @@ -287,7 +292,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminUpdateUserAttributes(params) if err != nil { - if tfawserr.ErrMessageContains(err, "ResourceNotFoundException", "") { + if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { log.Printf("[WARN] Cognito User %s is already gone", d.Id()) d.SetId("") return nil @@ -303,7 +308,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminDeleteUserAttributes(params) if err != nil { - if tfawserr.ErrMessageContains(err, "ResourceNotFoundException", "") { + if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { log.Printf("[WARN] Cognito User %s is already gone", d.Id()) d.SetId("") return nil @@ -436,20 +441,23 @@ func expandUserAttributesDelete(input []*string) []*string { } func flattenUserAttributes(apiList []*cognitoidentityprovider.AttributeType) map[string]interface{} { + // there is always the `sub` attribute + if len(apiList) == 1 { + return nil + } + tfMap := make(map[string]interface{}) - if len(apiList) > 1 { - for _, apiAttribute := range apiList { - if apiAttribute.Name != nil { - if UserAttributeKeyMatchesStandardAttribute(*apiAttribute.Name) { - if aws.StringValue(apiAttribute.Name) == "sub" { - continue - } - 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) + for _, apiAttribute := range apiList { + if apiAttribute.Name != nil { + if UserAttributeKeyMatchesStandardAttribute(*apiAttribute.Name) { + if aws.StringValue(apiAttribute.Name) == "sub" { + continue } + 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) } } } diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go new file mode 100644 index 000000000000..40c55da98c11 --- /dev/null +++ b/internal/service/cognitoidp/user_test.go @@ -0,0 +1,147 @@ +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.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), + resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.sms_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.software_token_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.preferred_mfa", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +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 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() == "ResourceNotFoundException" { + return nil + } + return err + } + } + + 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) +} From ba83f601da078fdea93e75d64b08cf0b0c00720a Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 2 Dec 2021 16:06:23 +0200 Subject: [PATCH 15/36] password and temp_password test --- internal/service/cognitoidp/user_test.go | 272 +++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index 40c55da98c11..d0b81ed76f90 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -72,6 +72,118 @@ func TestAccCognitoUser_disappears(t *testing.T) { }) } +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.TestCheckNoResourceAttr(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.TestCheckNoResourceAttr(userResourceName, "password"), + resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeConfirmed), + ), + }, + }, + }) +} + func testAccCheckUserExists(name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -133,6 +245,86 @@ func testAccCheckUserDestroy(s *terraform.State) error { 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" { @@ -145,3 +337,83 @@ resource "aws_cognito_user" "test" { } `, 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) +} From c335c297e3aa5011b891dc1c10ccf5fa8de1c0fc Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 2 Dec 2021 16:06:50 +0200 Subject: [PATCH 16/36] fixes to password updates; typo --- internal/service/cognitoidp/user.go | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 214b53581bd3..c9de37fc4f71 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -268,7 +268,7 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { } if err := d.Set("mfa_preference", flattenUserMfaPreference(user.MFAOptions, user.UserMFASettingList, user.PreferredMfaSetting)); err != nil { - return fmt.Errorf("failed settings user mfa_preference: %w", err) + return fmt.Errorf("failed setting user mfa_preference: %w", err) } return nil @@ -342,33 +342,43 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } } - if d.HasChange("temporary_password") || d.HasChange("password") { - tempPassword := d.Get("temporary_password").(string) - permanentPassword := d.Get("password").(string) + if d.HasChange("temporary_password") { + password := d.Get("temporary_password").(string) - var password string - var isPermanent bool + 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), + } - // both passwords cannot be non-empty because of ConflictsWith - if tempPassword != "" { - password = tempPassword - } else if permanentPassword != "" { - password = permanentPassword - isPermanent = true + _, err := conn.AdminSetUserPassword(setPasswordParams) + if err != nil { + return fmt.Errorf("Error changing Cognito User's password: %s", err) + } + } else { + d.Set("temporary_password", nil) } + } + + if d.HasChange("password") { + password := d.Get("password").(string) if password != "" { - tempPasswordParams := &cognitoidentityprovider.AdminSetUserPasswordInput{ + 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(isPermanent), + Permanent: aws.Bool(true), } - _, err := conn.AdminSetUserPassword(tempPasswordParams) + _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { return fmt.Errorf("Error changing Cognito User's password: %s", err) } + } else { + d.Set("password", nil) } } From 27b995771d4fc0ff502e4b4f9b6d5a641469e410 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 2 Dec 2021 16:07:23 +0200 Subject: [PATCH 17/36] ignore fields in basic test import --- internal/service/cognitoidp/user_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index d0b81ed76f90..e17214c25158 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -33,6 +33,7 @@ func TestAccCognitoUser_basic(t *testing.T) { testAccCheckUserExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "creation_date"), resource.TestCheckResourceAttrSet(resourceName, "last_modified_date"), + resource.TestCheckResourceAttrSet(resourceName, "sub"), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), resource.TestCheckResourceAttr(resourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.sms_enabled", "false"), @@ -44,6 +45,14 @@ func TestAccCognitoUser_basic(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "temporary_password", + "password", + "client_metadata", + "validation_data", + "desired_delivery_mediums", + "message_action", + }, }, }, }) From bb9374bd80f9b7077db64b2a2a388a04b61e7bd7 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 2 Dec 2021 17:35:30 +0200 Subject: [PATCH 18/36] test attributes --- internal/service/cognitoidp/user_test.go | 152 +++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index e17214c25158..df3da36516c9 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -193,6 +193,54 @@ func TestAccCognitoUser_password(t *testing.T) { }) } +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.%", "3"), + 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.%", "3"), + resource.TestCheckResourceAttr(resourceName, "attributes.two", "2"), + resource.TestCheckResourceAttr(resourceName, "attributes.three", "three"), + resource.TestCheckResourceAttr(resourceName, "attributes.four", "4"), + ), + }, + }, + }) +} + func testAccCheckUserExists(name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -426,3 +474,107 @@ resource "aws_cognito_user" "test" { } `, 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) +} From ac0777ac0dcde4a64e7602348bfcd01393009bfd Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 2 Dec 2021 17:52:07 +0200 Subject: [PATCH 19/36] test enabled --- internal/service/cognitoidp/user_test.go | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index df3da36516c9..a4e68e31a156 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -241,6 +241,48 @@ func TestAccCognitoUser_attributes(t *testing.T) { }) } +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] @@ -578,3 +620,17 @@ resource "aws_cognito_user" "test" { } `, 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 = %t +} +`, userPoolName, userName, enabled) +} From 3a29ec98d0b3016aa3a52aba173f3110875380e3 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 3 Dec 2021 13:47:16 +0200 Subject: [PATCH 20/36] Remove creation_date and last modified date According to common_review_items doc those fields are generally ommited for the sake of simplicity. --- internal/service/cognitoidp/user.go | 17 ----------------- internal/service/cognitoidp/user_test.go | 2 -- 2 files changed, 19 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index c9de37fc4f71..2bada8de46b4 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" @@ -40,10 +39,6 @@ func ResourceUser() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, }, - "creation_date": { - Type: schema.TypeString, - Computed: true, - }, "desired_delivery_mediums": { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -64,10 +59,6 @@ func ResourceUser() *schema.Resource { Type: schema.TypeBool, Optional: true, }, - "last_modified_date": { - Type: schema.TypeString, - Computed: true, - }, "message_action": { Type: schema.TypeString, Optional: true, @@ -259,14 +250,6 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("failed setting user enabled status: %w", err) } - if err := d.Set("creation_date", user.UserCreateDate.Format(time.RFC3339)); err != nil { - return fmt.Errorf("failed setting user creation_date: %w", err) - } - - if err := d.Set("last_modified_date", user.UserLastModifiedDate.Format(time.RFC3339)); err != nil { - return fmt.Errorf("failed setting user last_modified_date: %w", err) - } - if err := d.Set("mfa_preference", flattenUserMfaPreference(user.MFAOptions, user.UserMFASettingList, user.PreferredMfaSetting)); err != nil { return fmt.Errorf("failed setting user mfa_preference: %w", err) } diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index a4e68e31a156..9991e60206ff 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -31,8 +31,6 @@ func TestAccCognitoUser_basic(t *testing.T) { 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, "enabled", "true"), resource.TestCheckResourceAttr(resourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), From d52e577586b9a81348bed252e38b6ae93476ecbc Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 3 Dec 2021 15:20:50 +0200 Subject: [PATCH 21/36] add validate func for the username --- internal/service/cognitoidp/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 2bada8de46b4..98af779af387 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -96,6 +96,7 @@ func ResourceUser() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 128), }, "status": { Type: schema.TypeString, From c86b704ef8d86743b5dd433f94612aace8a679c0 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 3 Dec 2021 15:21:00 +0200 Subject: [PATCH 22/36] docs and changelog --- .changelog/19919.txt | 3 + website/docs/r/cognito_user.html.markdown | 95 +++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 .changelog/19919.txt create mode 100644 website/docs/r/cognito_user.html.markdown 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/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown new file mode 100644 index 000000000000..40816c1d60ad --- /dev/null +++ b/website/docs/r/cognito_user.html.markdown @@ -0,0 +1,95 @@ +--- +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) An array of name-value pairs that contain user attributes and attribute values to be set for the user to be created. +* `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`. 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). + +## Attributes Reference + +* `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 +``` From 5ba9a3b870a167f9bb07bbc2b8d70f7fdb834507 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 3 Dec 2021 15:22:55 +0200 Subject: [PATCH 23/36] formatting --- internal/service/cognitoidp/user.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 98af779af387..9e0b1b44fc24 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -93,9 +93,9 @@ func ResourceUser() *schema.Resource { ForceNew: true, }, "username": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, ValidateFunc: validation.StringLenBetween(1, 128), }, "status": { From f6daf63a6a34a4694ffaf2541d6ae1674a0838fe Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Fri, 3 Dec 2021 16:55:28 +0200 Subject: [PATCH 24/36] update errors check; update docs --- internal/service/cognitoidp/user.go | 5 +++-- website/docs/r/cognito_user.html.markdown | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 9e0b1b44fc24..a448104c2881 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -177,9 +177,10 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { resp, err := conn.AdminCreateUser(params) if err != nil { - if tfawserr.ErrMessageContains(err, "AliasExistsException", "") { + if tfawserr.ErrMessageContains(err, cognitoidentityprovider.ErrCodeUsernameExistsException, "An account with the email already exists") { log.Println("[ERROR] User alias already exists. To override the alias set `force_alias_creation` attribute to `true`.") - return nil + } else if tfawserr.ErrMessageContains(err, cognitoidentityprovider.ErrCodeInvalidParameterException, "No email provided but desired delivery medium was Email") { + log.Println("[ERROR] No email provided but desired delivery medium was `EMAIL`.") } return fmt.Errorf("Error creating Cognito User: %s", err) } diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown index 40816c1d60ad..b9a81e9668cb 100644 --- a/website/docs/r/cognito_user.html.markdown +++ b/website/docs/r/cognito_user.html.markdown @@ -72,7 +72,7 @@ The following arguments are optional: * `attributes` - (Optional) An array of name-value pairs that contain user attributes and attribute values to be set for the user to be created. * `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`. More than one value can be specified. Amazon Cognito does not store the `desired_delivery_mediums` value. Defaults to `["SMS"]`. +* `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. From 3b2d087117f4f381ffafdcd0e091faddeebfd57b Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Mon, 6 Dec 2021 15:45:23 +0200 Subject: [PATCH 25/36] update tests to check for empty string --- internal/service/cognitoidp/user_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index 9991e60206ff..caa79ef0deb3 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -127,7 +127,7 @@ func TestAccCognitoUser_temporaryPassword(t *testing.T) { Config: testAccUserConfigNoPassword(rUserPoolName, rClientName, rUserName), Check: resource.ComposeTestCheckFunc( testAccCheckUserExists(userResourceName), - resource.TestCheckNoResourceAttr(userResourceName, "temporary_password"), + resource.TestCheckResourceAttr(userResourceName, "temporary_password", ""), resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeForceChangePassword), ), }, @@ -183,7 +183,7 @@ func TestAccCognitoUser_password(t *testing.T) { Config: testAccUserConfigNoPassword(rUserPoolName, rClientName, rUserName), Check: resource.ComposeTestCheckFunc( testAccCheckUserExists(userResourceName), - resource.TestCheckNoResourceAttr(userResourceName, "password"), + resource.TestCheckResourceAttr(userResourceName, "password", ""), resource.TestCheckResourceAttr(userResourceName, "status", cognitoidentityprovider.UserStatusTypeConfirmed), ), }, From 1952176d05448d8a1b073c9bab82583bc205fb7e Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 14 Dec 2021 15:46:17 +0200 Subject: [PATCH 26/36] PR fixes: refactoring --- internal/service/cognitoidp/user.go | 153 +++++++--------------- internal/service/cognitoidp/user_test.go | 11 +- website/docs/r/cognito_user.html.markdown | 3 +- 3 files changed, 57 insertions(+), 110 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index a448104c2881..9a99b641a5e0 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" @@ -39,14 +40,15 @@ func ResourceUser() *schema.Resource { 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([]string{ - cognitoidentityprovider.DeliveryMediumTypeSms, - cognitoidentityprovider.DeliveryMediumTypeEmail, - }, false), + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(cognitoidentityprovider.DeliveryMediumType_Values(), false), }, Optional: true, }, @@ -59,34 +61,26 @@ func ResourceUser() *schema.Resource { Type: schema.TypeBool, Optional: true, }, - "message_action": { + "last_modified_date": { Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - cognitoidentityprovider.MessageActionTypeResend, - cognitoidentityprovider.MessageActionTypeSuppress, - }, false), + Computed: true, + }, + "message_action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(cognitoidentityprovider.MessageActionType_Values(), false), }, - "mfa_preference": { - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "sms_enabled": { - Type: schema.TypeBool, - Computed: true, - }, - "software_token_enabled": { - Type: schema.TypeBool, - Computed: true, - }, - "preferred_mfa": { - Type: schema.TypeString, - Computed: true, - }, - }, + "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, @@ -161,14 +155,14 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { if v, ok := d.GetOk("attributes"); ok { attributes := v.(map[string]interface{}) - params.UserAttributes = expandUserAttributes(attributes) + 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 = expandUserAttributes(attributes) + params.ValidationData = expandAttribute(attributes) } if v, ok := d.GetOk("temporary_password"); ok { @@ -177,15 +171,10 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { resp, err := conn.AdminCreateUser(params) if err != nil { - if tfawserr.ErrMessageContains(err, cognitoidentityprovider.ErrCodeUsernameExistsException, "An account with the email already exists") { - log.Println("[ERROR] User alias already exists. To override the alias set `force_alias_creation` attribute to `true`.") - } else if tfawserr.ErrMessageContains(err, cognitoidentityprovider.ErrCodeInvalidParameterException, "No email provided but desired delivery medium was Email") { - log.Println("[ERROR] No email provided but desired delivery medium was `EMAIL`.") - } - return fmt.Errorf("Error creating Cognito User: %s", err) + return fmt.Errorf("Error creating Cognito User (%s): %w", d.Id(), err) } - d.SetId(fmt.Sprintf("%s/%s", *params.UserPoolId, *resp.User.Username)) + 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{ @@ -195,7 +184,7 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminDisableUser(disableParams) if err != nil { - return fmt.Errorf("Error disabling Cognito User: %s", err) + return fmt.Errorf("Error disabling Cognito User (%s): %w", d.Id(), err) } } @@ -209,7 +198,7 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error setting Cognito User's password: %s", err) + return fmt.Errorf("Error setting Cognito User's password (%s): %w", d.Id(), err) } } @@ -228,33 +217,28 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { user, err := conn.AdminGetUser(params) if err != nil { - if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { + 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", err) + 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: %w", err) + return fmt.Errorf("failed setting user attributes (%s): %w", d.Id(), err) } - if err := d.Set("status", user.UserStatus); err != nil { - return fmt.Errorf("failed setting user status: %w", 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) } - if err := d.Set("sub", flattenUserSub(user.UserAttributes)); err != nil { - return fmt.Errorf("failed setting user sub: %w", err) - } - - if err := d.Set("enabled", user.Enabled); err != nil { - return fmt.Errorf("failed setting user enabled status: %w", err) - } - - if err := d.Set("mfa_preference", flattenUserMfaPreference(user.MFAOptions, user.UserMFASettingList, user.PreferredMfaSetting)); err != nil { - return fmt.Errorf("failed setting user mfa_preference: %w", err) - } + d.Set("preferred_mfa_setting", aws.StringValue(user.PreferredMfaSetting)) + d.Set("status", aws.StringValue(user.UserStatus)) + d.Set("enabled", aws.BoolValue(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 } @@ -273,16 +257,11 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { params := &cognitoidentityprovider.AdminUpdateUserAttributesInput{ Username: aws.String(d.Get("username").(string)), UserPoolId: aws.String(d.Get("user_pool_id").(string)), - UserAttributes: expandUserAttributes(upd), + UserAttributes: expandAttribute(upd), } _, err := conn.AdminUpdateUserAttributes(params) if err != nil { - if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { - log.Printf("[WARN] Cognito User %s is already gone", d.Id()) - d.SetId("") - return nil - } - return fmt.Errorf("Error updating Cognito User Attributes: %s", err) + return fmt.Errorf("Error updating Cognito User Attributes (%s): %w", d.Id(), err) } } if len(del) > 0 { @@ -293,12 +272,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminDeleteUserAttributes(params) if err != nil { - if tfawserr.ErrMessageContains(err, "UserNotFoundException", "") { - log.Printf("[WARN] Cognito User %s is already gone", d.Id()) - d.SetId("") - return nil - } - return fmt.Errorf("Error updating Cognito User Attributes: %s", err) + return fmt.Errorf("Error updating Cognito User Attributes (%s): %w", d.Id(), err) } } } @@ -313,7 +287,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminEnableUser(enableParams) if err != nil { - return fmt.Errorf("Error enabling Cognito User: %s", err) + return fmt.Errorf("Error enabling Cognito User (%s): %w", d.Id(), err) } } else { disableParams := &cognitoidentityprovider.AdminDisableUserInput{ @@ -322,7 +296,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminDisableUser(disableParams) if err != nil { - return fmt.Errorf("Error disabling Cognito User: %s", err) + return fmt.Errorf("Error disabling Cognito User (%s): %w", d.Id(), err) } } } @@ -340,7 +314,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error changing Cognito User's password: %s", err) + return fmt.Errorf("Error changing Cognito User's temporary password (%s): %w", d.Id(), err) } } else { d.Set("temporary_password", nil) @@ -360,7 +334,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error changing Cognito User's password: %s", err) + return fmt.Errorf("Error changing Cognito User's password (%s): %w", d.Id(), err) } } else { d.Set("password", nil) @@ -382,7 +356,7 @@ func resourceUserDelete(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminDeleteUser(params) if err != nil { - return fmt.Errorf("Error deleting Cognito User: %s", err) + return fmt.Errorf("Error deleting Cognito User (%s): %w", d.Id(), err) } return nil @@ -400,7 +374,7 @@ func resourceUserImport(d *schema.ResourceData, meta interface{}) ([]*schema.Res return []*schema.ResourceData{d}, nil } -func expandUserAttributes(tfMap map[string]interface{}) []*cognitoidentityprovider.AttributeType { +func expandAttribute(tfMap map[string]interface{}) []*cognitoidentityprovider.AttributeType { if len(tfMap) == 0 { return nil } @@ -498,7 +472,7 @@ func expandUserDesiredDeliveryMediums(tfSet *schema.Set) []*string { return apiList } -func flattenUserSub(apiList []*cognitoidentityprovider.AttributeType) string { +func retrieveUserSub(apiList []*cognitoidentityprovider.AttributeType) string { for _, attr := range apiList { if aws.StringValue(attr.Name) == "sub" { return aws.StringValue(attr.Value) @@ -518,35 +492,6 @@ func expandUserClientMetadata(tfMap map[string]interface{}) map[string]*string { return apiMap } -func flattenUserMfaPreference(mfaOptions []*cognitoidentityprovider.MFAOptionType, mfaSettingsList []*string, preferredMfa *string) []interface{} { - preference := map[string]interface{}{} - - for _, setting := range mfaSettingsList { - v := aws.StringValue(setting) - - if v == cognitoidentityprovider.ChallengeNameTypeSmsMfa { - preference["sms_enabled"] = true - } else if v == cognitoidentityprovider.ChallengeNameTypeSoftwareTokenMfa { - preference["software_token_enabled"] = true - } - } - - if len(mfaOptions) > 0 { - // mfaOptions.DeliveryMediums can only have value SMS so we check only first element - if aws.StringValue(mfaOptions[0].DeliveryMedium) == cognitoidentityprovider.DeliveryMediumTypeSms { - preference["sms_enabled"] = true - } - } - - preference["preferred_mfa"] = aws.StringValue(preferredMfa) - - return []interface{}{preference} -} - -func userAttributeHash(attr interface{}) int { - return schema.HashString(attr.(map[string]interface{})["name"]) -} - func UserAttributeKeyMatchesStandardAttribute(input string) bool { if len(input) == 0 { return false diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index caa79ef0deb3..f578bb6bade3 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -31,12 +31,13 @@ func TestAccCognitoUser_basic(t *testing.T) { 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), - resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.sms_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.software_token_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "mfa_preference.0.preferred_mfa", ""), ), }, { @@ -332,7 +333,7 @@ func testAccCheckUserDestroy(s *terraform.State) error { _, err := conn.AdminGetUser(params) if err != nil { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { + if awsErr, ok := err.(awserr.Error); ok && (awsErr.Code() == cognitoidentityprovider.ErrCodeUserNotFoundException || awsErr.Code() == cognitoidentityprovider.ErrCodeResourceNotFoundException) { return nil } return err @@ -628,7 +629,7 @@ resource "aws_cognito_user_pool" "test" { resource "aws_cognito_user" "test" { user_pool_id = aws_cognito_user_pool.test.id username = %[2]q - enabled = %t + enabled = %[3]t } `, userPoolName, userName, enabled) } diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown index b9a81e9668cb..f8b9270a8514 100644 --- a/website/docs/r/cognito_user.html.markdown +++ b/website/docs/r/cognito_user.html.markdown @@ -13,6 +13,7 @@ Provides a Cognito User Resource. ## Example Usage ### Basic configuration + ```terraform resource "aws_cognito_user_pool" "example" { name = "MyExamplePool" @@ -70,7 +71,7 @@ The following arguments are required: The following arguments are optional: -* `attributes` - (Optional) An array of name-value pairs that contain user attributes and attribute values to be set for the user to be created. +* `attributes` - (Optional) A map that contains user attributes and attribute values to be set for the user to be created. * `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`. From 78517aeed710c475e8890f86067f51421280b5af Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 14 Dec 2021 18:26:33 +0200 Subject: [PATCH 27/36] Add DiffSupressFunc to the attributes field This ensures that the attributes array is fully saved in state while suppressing changes to delete the "sub" attribute, which all cognito users have. --- internal/service/cognitoidp/user.go | 16 ++++++++-------- internal/service/cognitoidp/user_test.go | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 9a99b641a5e0..1a5011d37f5b 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -33,6 +33,14 @@ func ResourceUser() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + log.Printf("[DEBUG] k = %v, old = %v, new = %v", k, old, new) + if k == "attributes.sub" || k == "attributes.%" { + return true + } + + return false + }, Optional: true, }, "client_metadata": { @@ -410,19 +418,11 @@ func expandUserAttributesDelete(input []*string) []*string { } func flattenUserAttributes(apiList []*cognitoidentityprovider.AttributeType) map[string]interface{} { - // there is always the `sub` attribute - if len(apiList) == 1 { - return nil - } - tfMap := make(map[string]interface{}) for _, apiAttribute := range apiList { if apiAttribute.Name != nil { if UserAttributeKeyMatchesStandardAttribute(*apiAttribute.Name) { - if aws.StringValue(apiAttribute.Name) == "sub" { - continue - } tfMap[aws.StringValue(apiAttribute.Name)] = aws.StringValue(apiAttribute.Value) } else { name := strings.TrimPrefix(strings.TrimPrefix(aws.StringValue(apiAttribute.Name), "dev:"), "custom:") diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index f578bb6bade3..ed9c3ac87e2f 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -207,7 +207,7 @@ func TestAccCognitoUser_attributes(t *testing.T) { Config: testAccUserConfigAttributes(rUserPoolName, rUserName), Check: resource.ComposeTestCheckFunc( testAccCheckUserExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "attributes.%", "3"), + resource.TestCheckResourceAttr(resourceName, "attributes.%", "4"), resource.TestCheckResourceAttr(resourceName, "attributes.one", "1"), resource.TestCheckResourceAttr(resourceName, "attributes.two", "2"), resource.TestCheckResourceAttr(resourceName, "attributes.three", "3"), @@ -230,7 +230,7 @@ func TestAccCognitoUser_attributes(t *testing.T) { Config: testAccUserConfigAttributesUpdated(rUserPoolName, rUserName), Check: resource.ComposeTestCheckFunc( testAccCheckUserExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "attributes.%", "3"), + resource.TestCheckResourceAttr(resourceName, "attributes.%", "4"), resource.TestCheckResourceAttr(resourceName, "attributes.two", "2"), resource.TestCheckResourceAttr(resourceName, "attributes.three", "three"), resource.TestCheckResourceAttr(resourceName, "attributes.four", "4"), From 9f4186a22ae8fa14fdf853af676203a1df8cecda Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 14 Dec 2021 18:32:59 +0200 Subject: [PATCH 28/36] adjust phrasing in docs --- website/docs/r/cognito_user.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown index f8b9270a8514..e1aac06e9fb7 100644 --- a/website/docs/r/cognito_user.html.markdown +++ b/website/docs/r/cognito_user.html.markdown @@ -71,7 +71,7 @@ The following arguments are required: The following arguments are optional: -* `attributes` - (Optional) A map that contains user attributes and attribute values to be set for the user to be created. +* `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`. From 66c3adf3cd7807d50bf6d65843e8f926541c58be Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Tue, 14 Dec 2021 19:16:33 +0200 Subject: [PATCH 29/36] include ClientMetadata in UpdateUserAttributes --- internal/service/cognitoidp/user.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 1a5011d37f5b..10ad8e73c935 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -267,6 +267,12 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { 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) From 321746de3f3b682211c26602b47437d4648ed6ca Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 22 Dec 2021 17:07:57 +0200 Subject: [PATCH 30/36] fix error message using not yet existing id --- internal/service/cognitoidp/user.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 10ad8e73c935..7ba350ab0fb5 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -138,9 +138,12 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { log.Print("[DEBUG] Creating Cognito User") + username := d.Get("username").(string) + userPoolId := d.Get("user_pool_id").(string) + params := &cognitoidentityprovider.AdminCreateUserInput{ - Username: aws.String(d.Get("username").(string)), - UserPoolId: aws.String(d.Get("user_pool_id").(string)), + Username: aws.String(username), + UserPoolId: aws.String(userPoolId), } if v, ok := d.GetOk("client_metadata"); ok { @@ -179,7 +182,7 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { resp, err := conn.AdminCreateUser(params) if err != nil { - return fmt.Errorf("Error creating Cognito User (%s): %w", d.Id(), err) + 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))) From 448c756c4fa4d87f7116dfd9026812e1efd2f774 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 22 Dec 2021 17:28:45 +0200 Subject: [PATCH 31/36] apply terrafmt --- internal/service/cognitoidp/user_test.go | 260 +++++++++++------------ 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/internal/service/cognitoidp/user_test.go b/internal/service/cognitoidp/user_test.go index ed9c3ac87e2f..1cb9d8199758 100644 --- a/internal/service/cognitoidp/user_test.go +++ b/internal/service/cognitoidp/user_test.go @@ -426,12 +426,12 @@ func testAccUserPassword(userResName string, clientResName string) resource.Test func testAccUserConfigBasic(userPoolName string, userName string) string { return fmt.Sprintf(` resource "aws_cognito_user_pool" "test" { - name = %[1]q + name = %[1]q } resource "aws_cognito_user" "test" { - user_pool_id = aws_cognito_user_pool.test.id - username = %[2]q + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q } `, userPoolName, userName) } @@ -439,26 +439,26 @@ resource "aws_cognito_user" "test" { 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 - } + 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"] + 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 + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q + temporary_password = %[4]q } `, userPoolName, clientName, userName, password) } @@ -466,26 +466,26 @@ resource "aws_cognito_user" "test" { 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 - } + 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"] + 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 + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q + password = %[4]q } `, userPoolName, clientName, userName, password) } @@ -493,25 +493,25 @@ resource "aws_cognito_user" "test" { 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 - } + 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"] + 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 + user_pool_id = aws_cognito_user_pool.test.id + username = %[3]q } `, userPoolName, clientName, userName) } @@ -519,51 +519,51 @@ resource "aws_cognito_user" "test" { 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 {} - } + 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 + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q - attributes = { - one = "1" - two = "2" - three = "3" - } + attributes = { + one = "1" + two = "2" + three = "3" + } } `, userPoolName, userName) } @@ -571,51 +571,51 @@ resource "aws_cognito_user" "test" { 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 {} - } + 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 + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q - attributes = { - two = "2" - three = "three" - four = "4" - } + attributes = { + two = "2" + three = "three" + four = "4" + } } `, userPoolName, userName) } @@ -623,13 +623,13 @@ resource "aws_cognito_user" "test" { func testAccUserConfigEnable(userPoolName string, userName string, enabled bool) string { return fmt.Sprintf(` resource "aws_cognito_user_pool" "test" { - name = %[1]q + name = %[1]q } resource "aws_cognito_user" "test" { - user_pool_id = aws_cognito_user_pool.test.id - username = %[2]q - enabled = %[3]t + user_pool_id = aws_cognito_user_pool.test.id + username = %[2]q + enabled = %[3]t } `, userPoolName, userName, enabled) } From baa486a59be6366c1514995d113eb43284706a3f Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 22 Dec 2021 17:34:02 +0200 Subject: [PATCH 32/36] fix attributes section in docs --- website/docs/r/cognito_user.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown index e1aac06e9fb7..509303671252 100644 --- a/website/docs/r/cognito_user.html.markdown +++ b/website/docs/r/cognito_user.html.markdown @@ -83,6 +83,8 @@ The following arguments are optional: ## 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. From 7013e9f5912a89ad5e2d7382020caa6e459ae805 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Wed, 22 Dec 2021 17:42:57 +0200 Subject: [PATCH 33/36] remove extraneous pointer conversions --- internal/service/cognitoidp/user.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 7ba350ab0fb5..c97dd8d75e95 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -244,9 +244,9 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("failed setting user's mfa settings (%s): %w", d.Id(), err) } - d.Set("preferred_mfa_setting", aws.StringValue(user.PreferredMfaSetting)) - d.Set("status", aws.StringValue(user.UserStatus)) - d.Set("enabled", aws.BoolValue(user.Enabled)) + 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)) From 8a2cce11a01f447a7af6a3e25a13f67b727728fe Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 20 Jan 2022 13:31:52 +0200 Subject: [PATCH 34/36] small logging refactor --- internal/service/cognitoidp/user.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index c97dd8d75e95..40478f8bfd54 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -136,8 +136,6 @@ func ResourceUser() *schema.Resource { func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).CognitoIDPConn - log.Print("[DEBUG] Creating Cognito User") - username := d.Get("username").(string) userPoolId := d.Get("user_pool_id").(string) @@ -180,9 +178,11 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { 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) + 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))) @@ -195,7 +195,7 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminDisableUser(disableParams) if err != nil { - return fmt.Errorf("Error disabling Cognito User (%s): %w", d.Id(), err) + return fmt.Errorf("error disabling Cognito User (%s): %w", d.Id(), err) } } @@ -209,7 +209,7 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error setting Cognito User's password (%s): %w", d.Id(), err) + return fmt.Errorf("error setting Cognito User's password (%s): %w", d.Id(), err) } } @@ -233,7 +233,7 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { d.SetId("") return nil } - return fmt.Errorf("Error reading Cognito User (%s): %w", d.Id(), err) + return fmt.Errorf("error reading Cognito User (%s): %w", d.Id(), err) } if err := d.Set("attributes", flattenUserAttributes(user.UserAttributes)); err != nil { @@ -278,7 +278,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminUpdateUserAttributes(params) if err != nil { - return fmt.Errorf("Error updating Cognito User Attributes (%s): %w", d.Id(), err) + return fmt.Errorf("error updating Cognito User Attributes (%s): %w", d.Id(), err) } } if len(del) > 0 { @@ -289,7 +289,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminDeleteUserAttributes(params) if err != nil { - return fmt.Errorf("Error updating Cognito User Attributes (%s): %w", d.Id(), err) + return fmt.Errorf("error updating Cognito User Attributes (%s): %w", d.Id(), err) } } } @@ -304,7 +304,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminEnableUser(enableParams) if err != nil { - return fmt.Errorf("Error enabling Cognito User (%s): %w", d.Id(), err) + return fmt.Errorf("error enabling Cognito User (%s): %w", d.Id(), err) } } else { disableParams := &cognitoidentityprovider.AdminDisableUserInput{ @@ -313,7 +313,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { } _, err := conn.AdminDisableUser(disableParams) if err != nil { - return fmt.Errorf("Error disabling Cognito User (%s): %w", d.Id(), err) + return fmt.Errorf("error disabling Cognito User (%s): %w", d.Id(), err) } } } @@ -331,7 +331,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error changing Cognito User's temporary password (%s): %w", d.Id(), err) + return fmt.Errorf("error changing Cognito User's temporary password (%s): %w", d.Id(), err) } } else { d.Set("temporary_password", nil) @@ -351,7 +351,7 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminSetUserPassword(setPasswordParams) if err != nil { - return fmt.Errorf("Error changing Cognito User's password (%s): %w", d.Id(), err) + return fmt.Errorf("error changing Cognito User's password (%s): %w", d.Id(), err) } } else { d.Set("password", nil) @@ -373,7 +373,7 @@ func resourceUserDelete(d *schema.ResourceData, meta interface{}) error { _, err := conn.AdminDeleteUser(params) if err != nil { - return fmt.Errorf("Error deleting Cognito User (%s): %w", d.Id(), err) + return fmt.Errorf("error deleting Cognito User (%s): %w", d.Id(), err) } return nil @@ -382,7 +382,7 @@ func resourceUserDelete(d *schema.ResourceData, meta interface{}) error { 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") + return nil, errors.New("error importing Cognito User. Must specify user_pool_id/username") } userPoolId := idSplit[0] name := idSplit[1] From 8b4db9de5211037e6cf6e01a392028d4dae837b7 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 20 Jan 2022 13:32:58 +0200 Subject: [PATCH 35/36] add a note about passwords clearing --- website/docs/r/cognito_user.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/r/cognito_user.html.markdown b/website/docs/r/cognito_user.html.markdown index 509303671252..4d3b45524359 100644 --- a/website/docs/r/cognito_user.html.markdown +++ b/website/docs/r/cognito_user.html.markdown @@ -81,6 +81,8 @@ The following arguments are optional: * `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: From 1688fb761dc7a63049a73580570c850726076c56 Mon Sep 17 00:00:00 2001 From: Ruslan Timofieiev Date: Thu, 20 Jan 2022 14:21:34 +0200 Subject: [PATCH 36/36] Remove dev debug log message --- internal/service/cognitoidp/user.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/service/cognitoidp/user.go b/internal/service/cognitoidp/user.go index 40478f8bfd54..1c544b37cdb0 100644 --- a/internal/service/cognitoidp/user.go +++ b/internal/service/cognitoidp/user.go @@ -34,7 +34,6 @@ func ResourceUser() *schema.Resource { Type: schema.TypeString, }, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - log.Printf("[DEBUG] k = %v, old = %v, new = %v", k, old, new) if k == "attributes.sub" || k == "attributes.%" { return true }