diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 30c31c31..14f60f08 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -31,6 +31,7 @@ const ( roleSuperuserAttr = "superuser" roleValidUntilAttr = "valid_until" roleRolesAttr = "roles" + roleRoleParameterAttr = "role_parameter" roleSearchPathAttr = "search_path" roleStatementTimeoutAttr = "statement_timeout" @@ -74,6 +75,11 @@ func resourcePostgreSQLRole() *schema.Resource { MinItems: 0, Description: "Role(s) to grant to this new role", }, + roleRoleParameterAttr: { + Type: schema.TypeString, + Optional: true, + Description: "Sets a ROLE parameter on this role", + }, roleSearchPathAttr: { Type: schema.TypeList, Optional: true, @@ -290,6 +296,10 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro return err } + if err = alterRoleParameter(txn, d); err != nil { + return err + } + if err = setStatementTimeout(txn, d); err != nil { return err } @@ -448,6 +458,7 @@ func resourcePostgreSQLRoleReadImpl(c *Client, d *schema.ResourceData) error { d.Set(roleReplicationAttr, roleBypassRLS) d.Set(roleRolesAttr, pgArrayToSet(roleRoles)) d.Set(roleSearchPathAttr, readSearchPath(roleConfig)) + d.Set(roleRoleParameterAttr, readRoleParameter(roleConfig)) statementTimeout, err := readStatementTimeout(roleConfig) if err != nil { @@ -480,6 +491,19 @@ func readSearchPath(roleConfig pq.ByteaArray) []string { return nil } +// readRoleParameter searches for a role entry in the rolconfig array. +// In case no such value is present, it returns empty string. +func readRoleParameter(roleConfig pq.ByteaArray) string { + for _, v := range roleConfig { + config := string(v) + if strings.HasPrefix(config, "role=") { + var result = strings.TrimPrefix(config, "role=") + return result + } + } + return "" +} + // readStatementTimeout searches for a statement_timeout entry in the rolconfig array. // In case no such value is present, it returns nil. func readStatementTimeout(roleConfig pq.ByteaArray) (int, error) { @@ -624,6 +648,10 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro return err } + if err = alterRoleParameter(txn, d); err != nil { + return err + } + if err = setStatementTimeout(txn, d); err != nil { return err } @@ -951,3 +979,24 @@ func setStatementTimeout(txn *sql.Tx, d *schema.ResourceData) error { } return nil } + +func alterRoleParameter(txn *sql.Tx, d *schema.ResourceData) error { + roleName := d.Get(roleNameAttr).(string) + roleParameter := d.Get(roleRoleParameterAttr).(string) + if len(roleParameter) > 0 { + sql := fmt.Sprintf( + "ALTER ROLE %s SET role TO %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(roleParameter), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not set role parameter %s for %s: %w", roleParameter, roleName, err) + } + } else { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET role", pq.QuoteIdentifier(roleName), + ) + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("could not reset role parameter for %s: %w", roleName, err) + } + } + return nil +} diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index 9b361c06..5786091d 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -24,12 +24,12 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { { Config: testAccPostgresqlRoleConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("myrole2", nil, nil), + testAccCheckPostgresqlRoleExists("myrole2", nil, nil, ""), resource.TestCheckResourceAttr("postgresql_role.myrole2", "name", "myrole2"), resource.TestCheckResourceAttr("postgresql_role.myrole2", "login", "true"), resource.TestCheckResourceAttr("postgresql_role.myrole2", "roles.#", "0"), - testAccCheckPostgresqlRoleExists("role_default", nil, nil), + testAccCheckPostgresqlRoleExists("role_default", nil, nil, ""), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "name", "role_default"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "superuser", "false"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "create_database", "false"), @@ -51,11 +51,13 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { resource.TestCheckResourceAttr("postgresql_role.role_with_superuser", "superuser", "true"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "roles.#", "0"), - testAccCheckPostgresqlRoleExists("sub_role", []string{"myrole2", "role_simple"}, nil), + testAccCheckPostgresqlRoleExists("sub_role", []string{"myrole2", "role_simple"}, nil, ""), resource.TestCheckResourceAttr("postgresql_role.sub_role", "name", "sub_role"), resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.#", "2"), - testAccCheckPostgresqlRoleExists("role_with_search_path", nil, []string{"bar", "foo"}), + testAccCheckPostgresqlRoleExists("role_with_search_path", nil, []string{"bar", "foo"}, ""), + + testAccCheckPostgresqlRoleExists("role_with_role_parameter", nil, nil, "myrole2"), // The int part in the attr name is the schema.HashString of the value. resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.719783566", "myrole2"), @@ -88,6 +90,7 @@ resource "postgresql_role" "update_role" { connection_limit = 5 password = "titi" roles = ["${postgresql_role.group_role.name}"] + role_parameter = "${postgresql_role.group_role.id}" search_path = ["mysearchpath"] statement_timeout = 30000 } @@ -103,7 +106,7 @@ resource "postgresql_role" "update_role" { { Config: configCreate, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("update_role", []string{}, nil), + testAccCheckPostgresqlRoleExists("update_role", []string{}, nil, ""), resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "update_role"), resource.TestCheckResourceAttr("postgresql_role.update_role", "login", "true"), resource.TestCheckResourceAttr("postgresql_role.update_role", "connection_limit", "-1"), @@ -118,7 +121,7 @@ resource "postgresql_role" "update_role" { { Config: configUpdate, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("update_role2", []string{"group_role"}, nil), + testAccCheckPostgresqlRoleExists("update_role2", []string{"group_role"}, nil, "group_role"), resource.TestCheckResourceAttr( "postgresql_role.update_role", "name", "update_role2", ), @@ -134,6 +137,7 @@ resource "postgresql_role" "update_role" { resource.TestCheckResourceAttr("postgresql_role.update_role", "search_path.#", "1"), resource.TestCheckResourceAttr("postgresql_role.update_role", "search_path.0", "mysearchpath"), resource.TestCheckResourceAttr("postgresql_role.update_role", "statement_timeout", "30000"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "role_parameter", "group_role"), testAccCheckRoleCanLogin(t, "update_role2", "titi"), ), }, @@ -142,7 +146,7 @@ resource "postgresql_role" "update_role" { { Config: configCreate, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("update_role", []string{}, nil), + testAccCheckPostgresqlRoleExists("update_role", []string{}, nil, ""), resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "update_role"), resource.TestCheckResourceAttr("postgresql_role.update_role", "login", "true"), resource.TestCheckResourceAttr("postgresql_role.update_role", "connection_limit", "-1"), @@ -179,7 +183,7 @@ func testAccCheckPostgresqlRoleDestroy(s *terraform.State) error { return nil } -func testAccCheckPostgresqlRoleExists(roleName string, grantedRoles []string, searchPath []string) resource.TestCheckFunc { +func testAccCheckPostgresqlRoleExists(roleName string, grantedRoles []string, searchPath []string, roleParameter string) resource.TestCheckFunc { return func(s *terraform.State) error { client := testAccProvider.Meta().(*Client) @@ -203,6 +207,11 @@ func testAccCheckPostgresqlRoleExists(roleName string, grantedRoles []string, se return err } } + + if err := checkRoleParameter(client, roleName, roleParameter); err != nil { + return err + } + return nil } } @@ -268,7 +277,7 @@ func checkGrantedRoles(client *Client, roleName string, expectedRoles []string) func checkSearchPath(client *Client, roleName string, expectedSearchPath []string) error { var searchPathStr string err := client.DB().QueryRow( - "SELECT (pg_options_to_table(rolconfig)).option_value FROM pg_roles WHERE rolname=$1;", + "SELECT option_value FROM pg_roles r, pg_options_to_table(r.rolconfig) WHERE option_name='search_path' AND rolname=$1;", roleName, ).Scan(&searchPathStr) @@ -290,6 +299,29 @@ func checkSearchPath(client *Client, roleName string, expectedSearchPath []strin return nil } +func checkRoleParameter(client *Client, roleName string, expectedRoleParameter string) error { + var roleParameterStr string + err := client.DB().QueryRow( + "SELECT option_value FROM pg_roles r, pg_options_to_table(r.rolconfig) WHERE option_name='role' AND rolname=$1;", + roleName, + ).Scan(&roleParameterStr) + + // The query returns ErrNoRows if the role parameter is not found. + if err != nil && err == sql.ErrNoRows { + roleParameterStr = "" + } else if err != nil { + return fmt.Errorf("Error reading role: %v", err) + } + + if expectedRoleParameter != roleParameterStr { + return fmt.Errorf( + "role is not equal to expected value. expected %v - got %v", + expectedRoleParameter, roleParameterStr, + ) + } + return nil +} + var testAccPostgresqlRoleConfig = ` resource "postgresql_role" "myrole2" { name = "myrole2" @@ -349,6 +381,12 @@ resource "postgresql_role" "role_with_search_path" { search_path = ["bar", "foo"] } +resource "postgresql_role" "role_with_role_parameter" { + name = "role_with_role_parameter" + role_parameter = "${postgresql_role.myrole2.id}" + roles = ["${postgresql_role.myrole2.id}"] +} + resource "postgresql_role" "role_with_superuser" { name = "role_with_superuser" superuser = true diff --git a/website/docs/r/postgresql_role.html.markdown b/website/docs/r/postgresql_role.html.markdown index 97df8cb8..9706fecb 100644 --- a/website/docs/r/postgresql_role.html.markdown +++ b/website/docs/r/postgresql_role.html.markdown @@ -85,6 +85,9 @@ resource "postgresql_role" "my_replication_role" { * `password` - (Optional) Sets the role's password. A password is only of use for roles having the `login` attribute set to true. +* `role_parameter` - (Optional) Sets the `role` parameter for this role. The specified role + must be a part of `roles` list, unless this is a superuser. + * `roles` - (Optional) Defines list of roles which will be granted to this new role. * `search_path` - (Optional) Alters the search path of this new role. Note that