From a51d88822fabd6654d6b225323202964896b23aa Mon Sep 17 00:00:00 2001 From: Alex Hemard Date: Mon, 11 Dec 2023 12:53:20 -0600 Subject: [PATCH] feat(Cloud Databases): Redis Database User RBAC support Signed-off-by: Alex Hemard --- .secrets.baseline | 50 +++- go.mod | 2 +- go.sum | 4 +- ibm/service/database/resource_ibm_database.go | 229 ++++++++++++++---- .../resource_ibm_database_redis_test.go | 22 +- .../database/resource_ibm_database_test.go | 90 ++++++- website/docs/r/database.html.markdown | 2 +- 7 files changed, 327 insertions(+), 72 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index f7d14389539..ca2726d6a51 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.mod|go.sum|.*.map|^.secrets.baseline$", "lines": null }, - "generated_at": "2023-11-29T02:05:13Z", + "generated_at": "2023-12-14T01:01:48Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -2056,7 +2056,7 @@ "hashed_secret": "deab23f996709b4e3d14e5499d1cc2de677bfaa8", "is_secret": false, "is_verified": false, - "line_number": 1334, + "line_number": 1367, "type": "Secret Keyword", "verified_result": null }, @@ -2064,7 +2064,7 @@ "hashed_secret": "20a25bac21219ffff1904bde871ded4027eca2f8", "is_secret": false, "is_verified": false, - "line_number": 1923, + "line_number": 1957, "type": "Secret Keyword", "verified_result": null }, @@ -2072,7 +2072,7 @@ "hashed_secret": "b732fb611fd46a38e8667f9972e0cde777fbe37f", "is_secret": false, "is_verified": false, - "line_number": 1942, + "line_number": 1976, "type": "Secret Keyword", "verified_result": null }, @@ -2080,7 +2080,7 @@ "hashed_secret": "1f5e25be9b575e9f5d39c82dfd1d9f4d73f1975c", "is_secret": false, "is_verified": false, - "line_number": 2155, + "line_number": 2189, "type": "Secret Keyword", "verified_result": null } @@ -2224,7 +2224,15 @@ "hashed_secret": "2317aa72dafa0a07f05af47baa2e388f95dcf6f3", "is_secret": false, "is_verified": false, - "line_number": 272, + "line_number": 273, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "44cdfc3615970ada14420caaaa5c5745fca06002", + "is_secret": false, + "is_verified": false, + "line_number": 277, "type": "Secret Keyword", "verified_result": null } @@ -2234,7 +2242,7 @@ "hashed_secret": "c237978e1983e0caf1c3a84f1c2e72a7fb2981f2", "is_secret": false, "is_verified": false, - "line_number": 19, + "line_number": 20, "type": "Secret Keyword", "verified_result": null }, @@ -2242,7 +2250,7 @@ "hashed_secret": "d67007844d8f7fbc45ea3b27c4bea0bffafb53a0", "is_secret": false, "is_verified": false, - "line_number": 27, + "line_number": 28, "type": "Secret Keyword", "verified_result": null }, @@ -2250,7 +2258,7 @@ "hashed_secret": "279fb854eb9fa001b4629518a45c921cfad6d697", "is_secret": false, "is_verified": false, - "line_number": 35, + "line_number": 36, "type": "Secret Keyword", "verified_result": null }, @@ -2258,7 +2266,7 @@ "hashed_secret": "dad6fac3e5b6be7bb6f274970b4c50739a7e26ee", "is_secret": false, "is_verified": false, - "line_number": 59, + "line_number": 60, "type": "Secret Keyword", "verified_result": null }, @@ -2266,7 +2274,7 @@ "hashed_secret": "8cbbbfad0206e5953901f679b0d26d583c4f5ffe", "is_secret": false, "is_verified": false, - "line_number": 67, + "line_number": 68, "type": "Secret Keyword", "verified_result": null }, @@ -2274,7 +2282,7 @@ "hashed_secret": "f5ecb30890399c7b1d1781583478aaa9d0b0c89d", "is_secret": false, "is_verified": false, - "line_number": 91, + "line_number": 92, "type": "Secret Keyword", "verified_result": null }, @@ -2282,7 +2290,23 @@ "hashed_secret": "6da9eab371358a331c59a76d80a0ffcd589fe3c9", "is_secret": false, "is_verified": false, - "line_number": 101, + "line_number": 102, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "1f5e25be9b575e9f5d39c82dfd1d9f4d73f1975c", + "is_secret": false, + "is_verified": false, + "line_number": 163, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "e03932ac8a17ed1819fe161fd253bf323e0e3ec9", + "is_secret": false, + "is_verified": false, + "line_number": 172, "type": "Secret Keyword", "verified_result": null } diff --git a/go.mod b/go.mod index 3bb6d000847..08b7bc226ac 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/IBM/apigateway-go-sdk v0.0.0-20210714141226-a5d5d49caaca github.com/IBM/appconfiguration-go-admin-sdk v0.3.0 github.com/IBM/appid-management-go-sdk v0.0.0-20210908164609-dd0e0eaf732f - github.com/IBM/cloud-databases-go-sdk v0.3.2 + github.com/IBM/cloud-databases-go-sdk v0.4.0 github.com/IBM/cloudant-go-sdk v0.0.43 github.com/IBM/code-engine-go-sdk v0.0.0-20231106200405-99e81b3ee752 github.com/IBM/container-registry-go-sdk v1.1.0 diff --git a/go.sum b/go.sum index 2deeef865a7..04d27c7cb73 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,8 @@ github.com/IBM/appconfiguration-go-admin-sdk v0.3.0 h1:OqFxnDxro0JiRwHBKytCcseY2 github.com/IBM/appconfiguration-go-admin-sdk v0.3.0/go.mod h1:xPxAYhr/uywUIDEo/JqWbkUdTryPdzRdYBfUpA5IjoE= github.com/IBM/appid-management-go-sdk v0.0.0-20210908164609-dd0e0eaf732f h1:4c1kqY4GqmkQ+tO03rneDb74Tv7BhTj8jDiDB1p8mdM= github.com/IBM/appid-management-go-sdk v0.0.0-20210908164609-dd0e0eaf732f/go.mod h1:d22kTYY7RYBWcQlZpqrSdshpB/lJ16viWS5Sbjtlc8s= -github.com/IBM/cloud-databases-go-sdk v0.3.2 h1:AUi7/xswqCwuXIlSyuXtDZJIm4d0ZicUBHhPrE9TnH0= -github.com/IBM/cloud-databases-go-sdk v0.3.2/go.mod h1:nCIVfeZnhBYIiwByT959dFP4VWUeNLxomDYy63tTC6M= +github.com/IBM/cloud-databases-go-sdk v0.4.0 h1:pmmMbJb/axolBEpCqq85idcZiimAOTacCyLUfAhXCqI= +github.com/IBM/cloud-databases-go-sdk v0.4.0/go.mod h1:nCIVfeZnhBYIiwByT959dFP4VWUeNLxomDYy63tTC6M= github.com/IBM/cloudant-go-sdk v0.0.43 h1:YxTy4RpAEezX32YIWnds76hrBREmO4u6IkBz1WylNuQ= github.com/IBM/cloudant-go-sdk v0.0.43/go.mod h1:WeYrJPaHTw19943ndWnVfwMIlZ5z0XUM2uEXNBrwZ1M= github.com/IBM/code-engine-go-sdk v0.0.0-20231106200405-99e81b3ee752 h1:S5NT0aKKUqd9hnIrPN/qUijKx9cZjJi3kfFpog0ByDA= diff --git a/ibm/service/database/resource_ibm_database.go b/ibm/service/database/resource_ibm_database.go index aa2808dcbec..8b70da011ac 100644 --- a/ibm/service/database/resource_ibm_database.go +++ b/ibm/service/database/resource_ibm_database.go @@ -14,6 +14,7 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "time" @@ -57,20 +58,53 @@ const ( ) const ( - redisRBACRoleRegexPattern = `([+-][a-z]+\s?)+` + redisRBACRoleRegexPattern = `[+-]@(?P[a-z]+)` ) type DatabaseUser struct { Username string Password string - Role string + Role *string Type string } +type databaseUserValidationError struct { + user *DatabaseUser + errs []error +} + +func (e *databaseUserValidationError) Error() string { + if len(e.errs) == 0 { + return "" + } + + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + + return fmt.Sprintf("database user (%s) validation error:\n%s", e.user.Username, string(b)) +} + +func (e *databaseUserValidationError) Unwrap() []error { + return e.errs +} + type userChange struct { Old, New *DatabaseUser } +func redisRBACAllowedRoles() []string { + return []string{"all", "admin", "read", "write"} +} + +func opsManagerRoles() []string { + return []string{"group_read_only", "group_data_access_admin"} +} + func retry(f func() error) (err error) { attempts := 3 @@ -296,11 +330,10 @@ func ResourceIBMDatabaseInstance() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"database", "ops_manager", "read_only_replica"}, false), }, "role": { - Description: "User role. Only available for ops_manager user type.", - Type: schema.TypeString, - Optional: true, - Sensitive: false, - ValidateFunc: validation.StringInSlice([]string{"group_read_only", "group_data_access_admin"}, false), + Description: "User role. Only available for ops_manager user type and Redis 6.0 and above.", + Type: schema.TypeString, + Optional: true, + Sensitive: false, }, }, }, @@ -1330,23 +1363,23 @@ func resourceIBMDatabaseInstanceCreate(context context.Context, d *schema.Resour adminUser := deployment.AdminUsernames["database"] - user := &clouddatabasesv5.APasswordSettingUser{ + user := &clouddatabasesv5.UserUpdatePasswordSetting{ Password: &adminPassword, } - changeUserPasswordOptions := &clouddatabasesv5.ChangeUserPasswordOptions{ + updateUserOptions := &clouddatabasesv5.UpdateUserOptions{ ID: core.StringPtr(instanceID), UserType: core.StringPtr("database"), Username: core.StringPtr(adminUser), User: user, } - changeUserPasswordResponse, response, err := cloudDatabasesClient.ChangeUserPassword(changeUserPasswordOptions) + updateUserResponse, response, err := cloudDatabasesClient.UpdateUser(updateUserOptions) if err != nil { - return diag.FromErr(fmt.Errorf("[ERROR] ChangeUserPassword (%s) failed %s\n%s", *changeUserPasswordOptions.Username, err, response)) + return diag.FromErr(fmt.Errorf("[ERROR] UpdateUser (%s) failed %s\n%s", *updateUserOptions.Username, err, response)) } - taskID := *changeUserPasswordResponse.Task.ID + taskID := *updateUserResponse.Task.ID _, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutCreate)) if err != nil { @@ -1919,23 +1952,24 @@ func resourceIBMDatabaseInstanceUpdate(context context.Context, d *schema.Resour if d.HasChange("adminpassword") { adminUser := d.Get("adminuser").(string) password := d.Get("adminpassword").(string) - user := &clouddatabasesv5.APasswordSettingUser{ + + user := &clouddatabasesv5.UserUpdatePasswordSetting{ Password: &password, } - changeUserPasswordOptions := &clouddatabasesv5.ChangeUserPasswordOptions{ + updateUserOptions := &clouddatabasesv5.UpdateUserOptions{ ID: core.StringPtr(instanceID), UserType: core.StringPtr("database"), Username: core.StringPtr(adminUser), User: user, } - changeUserPasswordResponse, response, err := cloudDatabasesClient.ChangeUserPassword(changeUserPasswordOptions) + updateUserResponse, response, err := cloudDatabasesClient.UpdateUser(updateUserOptions) if err != nil { - return diag.FromErr(fmt.Errorf("[ERROR] ChangeUserPassword (%s) failed %s\n%s", *changeUserPasswordOptions.Username, err, response)) + return diag.FromErr(fmt.Errorf("[ERROR] UpdateUser (%s) failed %s\n%s", *updateUserOptions.Username, err, response)) } - taskID := *changeUserPasswordResponse.Task.ID + taskID := *updateUserResponse.Task.ID _, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) if err != nil { @@ -2813,6 +2847,28 @@ func validateGroupsDiff(_ context.Context, diff *schema.ResourceDiff, meta inter } func validateUsersDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) (err error) { + service := diff.Get("service").(string) + + var versionStr string + var version int + + if _version, ok := diff.GetOk("version"); ok { + versionStr = _version.(string) + } + + if versionStr == "" { + // Latest Version + version = 0 + } else { + _v, err := strconv.ParseFloat(versionStr, 64) + + if err != nil { + return fmt.Errorf("invalid version: %s", versionStr) + } + + version = int(_v) + } + oldUsers, newUsers := diff.GetChange("users") userChanges := expandUserChanges(oldUsers.(*schema.Set).List(), newUsers.(*schema.Set).List()) @@ -2822,14 +2878,34 @@ func validateUsersDiff(_ context.Context, diff *schema.ResourceDiff, meta interf } if change.isCreate() || change.isUpdate() { - err = change.New.Validate() + err = change.New.ValidatePassword() + + if err != nil { + return err + } + + // TODO: Use Capability API + // RBAC roles supported for Redis 6.0 and above + if service == "databases-for-redis" && !(version > 0 && version < 6) { + err = change.New.ValidateRBACRole() + } else if service == "databases-for-mongodb" && change.New.Type == "ops_manager" { + err = change.New.ValidateOpsManagerRole() + } else { + if change.New.Role != nil { + if *change.New.Role != "" { + err = errors.New("role is not supported for this deployment or user type") + err = &databaseUserValidationError{user: change.New, errs: []error{err}} + } + } + } + if err != nil { return err } } } - return nil + return } func expandUsers(_users []interface{}) []*DatabaseUser { @@ -2845,10 +2921,17 @@ func expandUsers(_users []interface{}) []*DatabaseUser { user := DatabaseUser{ Username: tfUser["name"].(string), Password: tfUser["password"].(string), - Role: tfUser["role"].(string), Type: tfUser["type"].(string), } + // NOTE: cannot differentiate nil vs empty string + // https://github.com/hashicorp/terraform-plugin-sdk/issues/741 + if role, ok := tfUser["role"].(string); ok { + if tfUser["role"] != "" { + user.Role = &role + } + } + users = append(users, &user) } } @@ -2875,8 +2958,8 @@ func expandUserChanges(_oldUsers []interface{}, _newUsers []interface{}) (userCh userChanges = make([]*userChange, 0, len(userChangeMap)) - for _, user := range userChangeMap { - userChanges = append(userChanges, user) + for _, change := range userChangeMap { + userChanges = append(userChanges, change) } return userChanges @@ -2913,9 +2996,9 @@ func (u *DatabaseUser) Create(instanceID string, d *schema.ResourceData, meta in Password: core.StringPtr(u.Password), } - // User Role only for ops_manager user type - if u.Type == "ops_manager" && u.Role != "" { - userEntry.Role = core.StringPtr(u.Role) + // User Role only for ops_manager user type and Redis 6.0 and above + if u.Role != nil { + userEntry.Role = u.Role } createDatabaseUserOptions := &clouddatabasesv5.CreateDatabaseUserOptions{ @@ -2947,30 +3030,34 @@ func (u *DatabaseUser) Update(instanceID string, d *schema.ResourceData, meta in } // Attempt to update user password - passwordSettingUser := &clouddatabasesv5.APasswordSettingUser{ + user := &clouddatabasesv5.UserUpdate{ Password: core.StringPtr(u.Password), } - changeUserPasswordOptions := &clouddatabasesv5.ChangeUserPasswordOptions{ + if u.Role != nil { + user.Role = u.Role + } + + updateUserOptions := &clouddatabasesv5.UpdateUserOptions{ ID: &instanceID, UserType: core.StringPtr(u.Type), Username: core.StringPtr(u.Username), - User: passwordSettingUser, + User: user, } - changeUserPasswordResponse, response, err := cloudDatabasesClient.ChangeUserPassword(changeUserPasswordOptions) + updateUserResponse, response, err := cloudDatabasesClient.UpdateUser(updateUserOptions) // user was found but an error occurs while triggering task if err != nil || (response.StatusCode < 200 || response.StatusCode >= 300) { - return fmt.Errorf("[ERROR] ChangeUserPassword (%s) failed %w\n%s", *changeUserPasswordOptions.Username, err, response) + return fmt.Errorf("[ERROR] UpdateUser (%s) failed %w\n%s", *updateUserOptions.Username, err, response) } - taskID := *changeUserPasswordResponse.Task.ID + taskID := *updateUserResponse.Task.ID _, err = waitForDatabaseTaskComplete(taskID, d, meta, d.Timeout(schema.TimeoutUpdate)) if err != nil { return fmt.Errorf( - "[ERROR] Error waiting for database (%s) user (%s) create task to complete: %w", instanceID, *changeUserPasswordOptions.Username, err) + "[ERROR] Error waiting for database (%s) user (%s) create task to complete: %w", instanceID, *updateUserOptions.Username, err) } return nil @@ -3011,7 +3098,7 @@ func (u *DatabaseUser) isUpdatable() bool { return u.Type != "ops_manager" } -func (u *DatabaseUser) Validate() error { +func (u *DatabaseUser) ValidatePassword() (err error) { var errs []error var specialChars string @@ -3063,24 +3150,84 @@ func (u *DatabaseUser) Validate() error { } if len(errs) == 0 { - return nil + return } - var b []byte - for i, err := range errs { - if i > 0 { - b = append(b, '\n') + return &databaseUserValidationError{user: u, errs: errs} +} + +func (u *DatabaseUser) ValidateRBACRole() (err error) { + var errs []error + + if u.Role == nil || *u.Role == "" { + return + } + + if u.Type != "database" { + errs = append(errs, errors.New("role is only allowed for the database user")) + return &databaseUserValidationError{user: u, errs: errs} + } + + redisRBACCategoryRegex := regexp.MustCompile(redisRBACRoleRegexPattern) + redisRBACRoleRegex := regexp.MustCompile(fmt.Sprintf(`^(%s\s?)+$`, redisRBACRoleRegexPattern)) + + if !redisRBACRoleRegex.MatchString(*u.Role) { + errs = append(errs, errors.New("role must be in the format +@category or -@category")) + } + + matches := redisRBACCategoryRegex.FindAllStringSubmatch(*u.Role, -1) + + for _, match := range matches { + valid := false + role := match[1] + for _, allowed := range redisRBACAllowedRoles() { + if role == allowed { + valid = true + break + } } - b = append(b, err.Error()...) + + if !valid { + errs = append(errs, fmt.Errorf("role must contain only allowed categories: %s", strings.Join(redisRBACAllowedRoles()[:], ","))) + break + } + } + + if len(errs) == 0 { + return } - return fmt.Errorf("database user (%s) validation error:\n%w", u.Username, errors.New(string(b))) + return &databaseUserValidationError{user: u, errs: errs} +} + +func (u *DatabaseUser) ValidateOpsManagerRole() (err error) { + if u.Role == nil { + return + } + + if u.Type != "ops_manager" { + return + } + + if *u.Role == "" { + return + } + + for _, str := range opsManagerRoles() { + if *u.Role == str { + return + } + } + + err = fmt.Errorf("role must be a valid ops_manager role: %s", strings.Join(opsManagerRoles()[:], ",")) + + return &databaseUserValidationError{user: u, errs: []error{err}} } func DatabaseUserPasswordValidator(userType string) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { user := &DatabaseUser{Username: "admin", Type: userType, Password: i.(string)} - err := user.Validate() + err := user.ValidatePassword() if err != nil { errors = append(errors, err) } diff --git a/ibm/service/database/resource_ibm_database_redis_test.go b/ibm/service/database/resource_ibm_database_redis_test.go index 64eec792a76..22366d31fec 100644 --- a/ibm/service/database/resource_ibm_database_redis_test.go +++ b/ibm/service/database/resource_ibm_database_redis_test.go @@ -70,14 +70,15 @@ func TestAccIBMDatabaseInstance_Redis_Basic(t *testing.T) { ), }, { - Config: testAccCheckIBMDatabaseInstanceRedisGroupMigration(databaseResourceGroup, testName), + Config: testAccCheckIBMDatabaseInstanceRedisUserRole(databaseResourceGroup, testName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(name, "name", testName), resource.TestCheckResourceAttr(name, "service", "databases-for-redis"), resource.TestCheckResourceAttr(name, "plan", "standard"), resource.TestCheckResourceAttr(name, "location", acc.Region()), - resource.TestCheckResourceAttr(name, "groups.0.memory.0.allocation_mb", "2048"), - resource.TestCheckResourceAttr(name, "groups.0.disk.0.allocation_mb", "4096"), + resource.TestCheckResourceAttr(name, "users.#", "1"), + resource.TestCheckResourceAttr(name, "users.0.name", "coolguy"), + resource.TestCheckResourceAttr(name, "users.0.role", "-@all +@read"), resource.TestCheckResourceAttr(name, "allowlist.#", "0"), ), }, @@ -257,7 +258,7 @@ func testAccCheckIBMDatabaseInstanceRedisReduced(databaseResourceGroup string, n `, databaseResourceGroup, name, acc.Region()) } -func testAccCheckIBMDatabaseInstanceRedisGroupMigration(databaseResourceGroup string, name string) string { +func testAccCheckIBMDatabaseInstanceRedisUserRole(databaseResourceGroup string, name string) string { return fmt.Sprintf(` data "ibm_resource_group" "test_acc" { is_default = true @@ -271,15 +272,10 @@ func testAccCheckIBMDatabaseInstanceRedisGroupMigration(databaseResourceGroup st location = "%[3]s" adminpassword = "password12345678" - group { - group_id = "member" - - memory { - allocation_mb = 1024 - } - disk { - allocation_mb = 2048 - } + users { + name = "coolguy" + password = "securepassword123" + role = "-@all +@read" } } `, databaseResourceGroup, name, acc.Region()) diff --git a/ibm/service/database/resource_ibm_database_test.go b/ibm/service/database/resource_ibm_database_test.go index d5633469a8f..5e58458d583 100644 --- a/ibm/service/database/resource_ibm_database_test.go +++ b/ibm/service/database/resource_ibm_database_test.go @@ -4,6 +4,7 @@ package database import ( + "github.com/IBM/go-sdk-core/v5/core" "gotest.tools/assert" "testing" ) @@ -95,7 +96,7 @@ func TestValidateUserPassword(t *testing.T) { }, } for _, tc := range testcases { - err := tc.user.Validate() + err := tc.user.ValidatePassword() if tc.expectedError == "" { if err != nil { t.Errorf("TestValidateUserPassword: %q, %q unexpected error: %q", tc.user.Username, tc.user.Password, err.Error()) @@ -105,3 +106,90 @@ func TestValidateUserPassword(t *testing.T) { } } } + +func TestValidateRBACRole(t *testing.T) { + testcases := []struct { + user DatabaseUser + expectedError string + }{ + { + user: DatabaseUser{ + Username: "invalid_format", + Password: "", + Type: "database", + Role: core.StringPtr("+admin -all"), + }, + expectedError: "database user (invalid_format) validation error:\nrole must be in the format +@category or -@category", + }, + { + user: DatabaseUser{ + Username: "invalid_operation", + Password: "", + Type: "database", + Role: core.StringPtr("~@admin"), + }, + expectedError: "database user (invalid_operation) validation error:\nrole must be in the format +@category or -@category", + }, + { + user: DatabaseUser{ + Username: "invalid_category", + Password: "", + Type: "database", + Role: core.StringPtr("+@catfood -@dogfood"), + }, + expectedError: "database user (invalid_category) validation error:\nrole must contain only allowed categories: all,admin,read,write", + }, + { + user: DatabaseUser{ + Username: "one_bad_apple", + Password: "", + Type: "database", + Role: core.StringPtr("-@jazz +@read"), + }, + expectedError: "database user (one_bad_apple) validation error:\nrole must contain only allowed categories: all,admin,read,write", + }, + { + user: DatabaseUser{ + Username: "invalid_user_type", + Password: "", + Type: "ops_manager", + Role: core.StringPtr("+@all"), + }, + expectedError: "database user (invalid_user_type) validation error:\nrole is only allowed for the database user", + }, + { + user: DatabaseUser{ + Username: "valid", + Password: "", + Type: "database", + Role: core.StringPtr("-@all +@read"), + }, + expectedError: "", + }, + { + user: DatabaseUser{ + Username: "blank_role", + Password: "-@all +@read", + Type: "database", + Role: core.StringPtr(""), + }, + expectedError: "", + }, + } + for _, tc := range testcases { + err := tc.user.ValidateRBACRole() + if tc.expectedError == "" { + if err != nil { + t.Errorf("TestValidateRBACRole: %q, %q unexpected error: %q", tc.user.Username, *tc.user.Role, err.Error()) + } + } else { + var errMsg string + + if err != nil { + errMsg = err.Error() + } + + assert.Equal(t, tc.expectedError, errMsg) + } + } +} diff --git a/website/docs/r/database.html.markdown b/website/docs/r/database.html.markdown index b27f42deabd..cfa9e61d86f 100644 --- a/website/docs/r/database.html.markdown +++ b/website/docs/r/database.html.markdown @@ -684,7 +684,7 @@ Review the argument reference that you can specify for your resource. - `name` - (Required, String) The user name to add to the database instance. The user name must be in the range 5 - 32 characters. - `password` - (Required, String) The password for the user. Passwords must be between 15 and 32 characters in length and contain a letter and a number. Users with an `ops_manager` user type must have a password containing a special character `~!@#$%^&*()=+[]{}|;:,.<>/?_-` as well as a letter and a number. Other user types may only use special characters `-_`. - `type` - (Optional, String) The type for the user. Examples: `database`, `ops_manager`, `read_only_replica`. The default value is `database`. - - `role` - (Optional, String) The role for the user. Only available for `ops_manager` user type. Examples: `group_read_only`, `group_data_access_admin`. + - `role` - (Optional, String) The role for the user. Only available for `ops_manager` user type or Redis 6.0 and above. Example roles for `ops_manager`: `group_read_only`, `group_data_access_admin`. For, Redis 6.0 and above, `role` must be in Redis ACL syntax for adding and removing command categories i.e. `+@category` or `-@category`. Allowed command categories are `all`, `admin`, `read`, `write`. Example Redis `role`: `-@all +@read` - `allowlist` - (Optional, List of Objects) A list of allowed IP addresses for the database. Multiple blocks are allowed.