Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/48905.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_elasticache_user: Add `password_wo_1`, `password_wo_2`, and `password_wo_version` attributes for write-only password support with up to 2 simultaneous passwords
```

```release-note:note
resource/aws_elasticache_user: The `passwords_wo` and `passwords_wo_version` attributes are deprecated in favor of `password_wo_1`, `password_wo_2`, and `password_wo_version`
```
90 changes: 81 additions & 9 deletions internal/service/elasticache/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func resourceUser() *schema.Resource {
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"password_count": {
Type: schema.TypeInt,
Computed: true,
},
"passwords": {
Type: schema.TypeSet,
Optional: true,
Expand All @@ -77,10 +81,6 @@ func resourceUser() *schema.Resource {
Type: schema.TypeString,
},
},
"password_count": {
Type: schema.TypeInt,
Computed: true,
},
names.AttrType: {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -111,21 +111,47 @@ func resourceUser() *schema.Resource {
Type: schema.TypeString,
ValidateFunc: validation.StringLenBetween(16, 128),
},
Sensitive: true,
Sensitive: true,
ConflictsWith: []string{"password_wo_1", "password_wo_2"},
},
"password_wo_1": {
Type: schema.TypeString,
Optional: true,
WriteOnly: true,
Sensitive: true,
ValidateFunc: validation.StringLenBetween(16, 128),
ConflictsWith: []string{"passwords", "passwords_wo", "authentication_mode.0.passwords"},
RequiredWith: []string{"password_wo_version"},
},
"password_wo_2": {
Type: schema.TypeString,
Optional: true,
WriteOnly: true,
Sensitive: true,
ValidateFunc: validation.StringLenBetween(16, 128),
ConflictsWith: []string{"passwords", "passwords_wo", "authentication_mode.0.passwords"},
RequiredWith: []string{"password_wo_version"},
},
"password_wo_version": {
Type: schema.TypeInt,
Optional: true,
},
"passwords_wo": {
Type: schema.TypeString,
Optional: true,
WriteOnly: true,
Sensitive: true,
ValidateFunc: validation.StringLenBetween(16, 128),
ConflictsWith: []string{"passwords", "authentication_mode"},
ConflictsWith: []string{"passwords", "authentication_mode", "password_wo_1", "password_wo_2"},
RequiredWith: []string{"passwords_wo_version"},
Deprecated: "Use password_wo_1 and password_wo_2 instead",
},
"passwords_wo_version": {
Type: schema.TypeInt,
Optional: true,
RequiredWith: []string{"passwords_wo"},
Type: schema.TypeInt,
Optional: true,
RequiredWith: []string{"passwords_wo"},
ConflictsWith: []string{"password_wo_version"},
Deprecated: "Use password_wo_version instead",
},
names.AttrTags: tftags.TagsSchema(),
names.AttrTagsAll: tftags.TagsSchemaComputed(),
Expand Down Expand Up @@ -179,6 +205,16 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta any) d
input.Passwords = []string{passwordsWO}
}

// New: top-level password_wo_1 / password_wo_2
authModePasswords, di := getAuthModePasswordsWO(d)
diags = append(diags, di...)
if diags.HasError() {
return diags
}
if len(authModePasswords) > 0 {
input.Passwords = authModePasswords
}

output, err := conn.CreateUser(ctx, input)

// Some partitions (e.g. ISO) may not support tag-on-create.
Expand Down Expand Up @@ -297,6 +333,17 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta any) d
}
}

if d.HasChange("password_wo_version") {
authModePasswords, di := getAuthModePasswordsWO(d)
diags = append(diags, di...)
if diags.HasError() {
return diags
}
if len(authModePasswords) > 0 {
input.Passwords = authModePasswords
}
}

_, err := conn.ModifyUser(ctx, input)

if err != nil {
Expand Down Expand Up @@ -455,6 +502,31 @@ func waitUserDeleted(ctx context.Context, conn *elasticache.Client, id string, t
return nil, err
}

func getAuthModePasswordsWO(d *schema.ResourceData) ([]string, diag.Diagnostics) {
var diags diag.Diagnostics
var passwords []string

pw1, di := flex.GetWriteOnlyStringValue(d, cty.GetAttrPath("password_wo_1"))
diags = append(diags, di...)
if diags.HasError() {
return nil, diags
}
if pw1 != "" {
passwords = append(passwords, pw1)
}

pw2, di := flex.GetWriteOnlyStringValue(d, cty.GetAttrPath("password_wo_2"))
diags = append(diags, di...)
if diags.HasError() {
return nil, diags
}
if pw2 != "" {
passwords = append(passwords, pw2)
}

return passwords, diags
}

func expandAuthenticationMode(tfMap map[string]any) *awstypes.AuthenticationMode {
if tfMap == nil {
return nil
Expand Down
189 changes: 189 additions & 0 deletions internal/service/elasticache/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"testing"

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
Expand Down Expand Up @@ -660,3 +661,191 @@ resource "aws_elasticache_user" "test" {
}
`, rName, password, version)
}

func TestAccElastiCacheUser_passwordWOSingle(t *testing.T) {
ctx := acctest.Context(t)
var user awstypes.User
rName := acctest.RandomWithPrefix(t, "tf-acc")
resourceName := "aws_elasticache_user.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckUserDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccUserConfig_passwordWOSingle(rName, "password123456789", 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user),
resource.TestCheckResourceAttr(resourceName, "user_id", rName),
resource.TestCheckResourceAttr(resourceName, names.AttrUserName, "username1"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"password_wo_1",
"password_wo_version",
"no_password_required",
},
},
},
})
}

func TestAccElastiCacheUser_passwordWODual(t *testing.T) {
ctx := acctest.Context(t)
var user awstypes.User
rName := acctest.RandomWithPrefix(t, "tf-acc")
resourceName := "aws_elasticache_user.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckUserDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccUserConfig_passwordWODual(rName, "password123456789", "password987654321", 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user),
resource.TestCheckResourceAttr(resourceName, "user_id", rName),
resource.TestCheckResourceAttr(resourceName, names.AttrUserName, "username1"),
resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.type", names.AttrPassword),
resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.password_count", "2"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"password_wo_1",
"password_wo_2",
"password_wo_version",
"no_password_required",
},
},
},
})
}

func TestAccElastiCacheUser_passwordWORotation(t *testing.T) {
ctx := acctest.Context(t)
var user1, user2 awstypes.User
rName := acctest.RandomWithPrefix(t, "tf-acc")
resourceName := "aws_elasticache_user.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckUserDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccUserConfig_passwordWOSingle(rName, "password123456789", 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user1),
),
},
// Update password by incrementing version
{
Config: testAccUserConfig_passwordWOSingle(rName, "newpassword1234567", 2),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user2),
),
},
},
})
}

func TestAccElastiCacheUser_passwordWOReduceToOne(t *testing.T) {
ctx := acctest.Context(t)
var user1, user2 awstypes.User
rName := acctest.RandomWithPrefix(t, "tf-acc")
resourceName := "aws_elasticache_user.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckUserDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccUserConfig_passwordWODual(rName, "password123456789", "password987654321", 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user1),
),
},
// Reduce to single password by incrementing version
{
Config: testAccUserConfig_passwordWOSingle(rName, "password123456789", 2),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserExists(ctx, t, resourceName, &user2),
),
},
},
})
}

func TestAccElastiCacheUser_passwordWOConflictsWithPasswords(t *testing.T) {
ctx := acctest.Context(t)
rName := acctest.RandomWithPrefix(t, "tf-acc")

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckUserDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccUserConfig_passwordWOConflict(rName),
ExpectError: regexache.MustCompile(`"password_wo_1": conflicts with passwords`),
},
},
})
}

func testAccUserConfig_passwordWOSingle(rName, password string, version int) string {
return fmt.Sprintf(`
resource "aws_elasticache_user" "test" {
user_id = %[1]q
user_name = "username1"
access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember"
engine = "redis"
password_wo_1 = %[2]q
password_wo_version = %[3]d
}
`, rName, password, version)
}

func testAccUserConfig_passwordWODual(rName, password1, password2 string, version int) string {
return fmt.Sprintf(`
resource "aws_elasticache_user" "test" {
user_id = %[1]q
user_name = "username1"
access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember"
engine = "redis"
password_wo_1 = %[2]q
password_wo_2 = %[3]q
password_wo_version = %[4]d
}
`, rName, password1, password2, version)
}

func testAccUserConfig_passwordWOConflict(rName string) string {
return fmt.Sprintf(`
resource "aws_elasticache_user" "test" {
user_id = %[1]q
user_name = "username1"
access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember"
engine = "redis"
passwords = ["aaaaaaaaaaaaaaaa"]
password_wo_1 = "bbbbbbbbbbbbbbbb"
password_wo_version = 1
}
`, rName)
}
Loading