diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 30c31c31..3a12d839 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -25,6 +25,7 @@ const ( roleLoginAttr = "login" roleNameAttr = "name" rolePasswordAttr = "password" + roleParametersAttr = "parameters" roleReplicationAttr = "replication" roleSkipDropRoleAttr = "skip_drop_role" roleSkipReassignOwnedAttr = "skip_reassign_owned" @@ -38,6 +39,8 @@ const ( roleDepEncryptedAttr = "encrypted" ) +var roleStandaloneParameters = [...]string{roleSearchPathAttr, roleStatementTimeoutAttr} + func resourcePostgreSQLRole() *schema.Resource { return &schema.Resource{ Create: resourcePostgreSQLRoleCreate, @@ -160,6 +163,13 @@ func resourcePostgreSQLRole() *schema.Resource { Description: "Abort any statement that takes more than the specified number of milliseconds", ValidateFunc: validation.IntAtLeast(0), }, + roleParametersAttr: { + Type: schema.TypeMap, + Optional: true, + Description: "Specifies default parameters which will be set for the role", + Elem: &schema.Schema{Type: schema.TypeString}, + ValidateFunc: validateRoleParameters, + }, }, } } @@ -294,6 +304,10 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro return err } + if err = setRoleParameters(txn, d); err != nil { + return err + } + if err = txn.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -458,6 +472,11 @@ func resourcePostgreSQLRoleReadImpl(c *Client, d *schema.ResourceData) error { d.SetId(roleName) + err = getRoleParameters(c.DB(), d) + if err != nil { + return err + } + password, err := readRolePassword(c, d, roleCanLogin) if err != nil { return err @@ -628,6 +647,10 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro return err } + if err = alterRoleParameters(txn, d); err != nil { + return err + } + if err = txn.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -900,6 +923,134 @@ func grantRoles(txn *sql.Tx, d *schema.ResourceData) error { return nil } +func isStandaloneRoleParameter(paramName string) bool { + for _, standaloneParameter := range roleStandaloneParameters { + if strings.ToLower(paramName) == strings.ToLower(standaloneParameter) { + return true + } + } + return false +} + +func validateRoleParameters(value interface{}, key string) (ws []string, es []error) { + paramValues := value.(map[string]interface{}) + + for p, v := range paramValues { + if len(v.(string)) == 0 { + es = append(es, fmt.Errorf("Parameter %s has no value", p)) + } + + if isStandaloneRoleParameter(p) { + es = append(es, fmt.Errorf("Parameter %s should be set by dedicated Role resource property", p)) + } + } + return +} + +func setRoleParameters(txn *sql.Tx, d *schema.ResourceData) error { + roleName := d.Get(roleNameAttr).(string) + roleParameters := d.Get(roleParametersAttr).(map[string]interface{}) + + for param, value := range roleParameters { + // We completely ignore parameters set by other properties + if isStandaloneRoleParameter(param) { + continue + } + + query := fmt.Sprintf( + "ALTER ROLE %s SET %s TO %s", pq.QuoteIdentifier(roleName), param, pq.QuoteLiteral(value.(string)), + ) + + fmt.Printf("Setting: %s\n", query) + + if _, err := txn.Exec(query); err != nil { + return fmt.Errorf("Could not set parameter %s for %s: %w", param, roleName, err) + } + } + + return nil +} + +func getRoleParameters(db *sql.DB, d *schema.ResourceData) error { + var params map[string]string + var err error + + if params, err = getRoleParametersImpl(db, d); err != nil { + return err + } + + return d.Set(roleParametersAttr, params) +} + +func alterRoleParameters(txn *sql.Tx, d *schema.ResourceData) error { + roleName := d.Get(roleNameAttr).(string) + roleParameters := d.Get(roleParametersAttr).(map[string]interface{}) + + var existingParams map[string]string + var err error + + if existingParams, err = getRoleParametersImpl(txn, d); err != nil { + return err + } + + // Unset parameters that currently exist, but are not supposed to be set + for param := range existingParams { + // We completely ignore parameters set by other properties + if isStandaloneRoleParameter(param) { + continue + } + + if _, ok := roleParameters[param]; !ok { + sql := fmt.Sprintf( + "ALTER ROLE %s RESET %s", pq.QuoteIdentifier(roleName), param, + ) + + fmt.Printf("Altering: %s\n", sql) + + if _, err := txn.Exec(sql); err != nil { + return fmt.Errorf("Could not reset %s parameter for %s: %w", param, roleName, err) + } + } + } + + // Now let's set actual parameters + return setRoleParameters(txn, d) +} + +func getRoleParametersImpl(db QueryAble, d *schema.ResourceData) (map[string]string, error) { + roleName := d.Get(roleNameAttr).(string) + + query := "SELECT option_name, option_value FROM (SELECT (pg_options_to_table(r.rolconfig)).* FROM pg_roles r where rolname = $1) t" + rows, err := db.Query(query, roleName) + + if err != nil { + return nil, fmt.Errorf("Unable to read role paramaters for %s: %w", roleName, err) + } + defer rows.Close() + + dbParams := make(map[string]string) + + for rows.Next() { + var ( + paramName string + paramValue string + ) + if err := rows.Scan(¶mName, ¶mValue); err != nil { + return nil, fmt.Errorf("Unable to read role paramaters for %s: %w", roleName, err) + } + + // We completely ignore parameters set by other properties + if isStandaloneRoleParameter(paramName) { + fmt.Printf("Ignored: %s, %s\n", paramName, paramValue) + continue + } + + fmt.Printf("Getting: %s, %s\n", paramName, paramValue) + dbParams[paramName] = paramValue + } + return dbParams, nil +} + func alterSearchPath(txn *sql.Tx, d *schema.ResourceData) error { role := d.Get(roleNameAttr).(string) searchPathInterface := d.Get(roleSearchPathAttr).([]interface{}) diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index 9b361c06..eda320fc 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -74,6 +74,9 @@ resource "postgresql_role" "update_role" { login = true password = "toto" valid_until = "2099-05-04 12:00:00+00" + parameters = { + application_name = "First_app" + } } ` @@ -90,6 +93,11 @@ resource "postgresql_role" "update_role" { roles = ["${postgresql_role.group_role.name}"] search_path = ["mysearchpath"] statement_timeout = 30000 + parameters = { + application_name = "Final" + log_statement = "all" + role = "${postgresql_role.group_role.name}" + } } ` resource.Test(t, resource.TestCase{ @@ -342,6 +350,12 @@ resource "postgresql_role" "sub_role" { "${postgresql_role.myrole2.id}", "${postgresql_role.role_simple.id}", ] + parameters = { + application_name = "aaa" + role = "${postgresql_role.myrole2.id}" + //setting this will raise validation error + //search_path = "aaa" + } } resource "postgresql_role" "role_with_search_path" {