diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index b7cb0fab..eb556927 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -35,11 +35,23 @@ const ( roleSearchPathAttr = "search_path" roleStatementTimeoutAttr = "statement_timeout" roleAssumeRoleAttr = "assume_role" + roleParameterAttr = "parameter" + roleParameterNameAttr = "name" + roleParameterValueAttr = "value" + roleParameterQuoteAttr = "quote" // Deprecated options roleDepEncryptedAttr = "encrypted" ) +// These parameters have discrete attributes, so they are not supported by the parameter block +var ignoredRoleConfigurationParameters = []string{ + roleSearchPathAttr, + roleIdleInTransactionSessionTimeoutAttr, + roleStatementTimeoutAttr, + "role", +} + func resourcePostgreSQLRole() *schema.Resource { return &schema.Resource{ Create: PGResourceFunc(resourcePostgreSQLRoleCreate), @@ -173,6 +185,36 @@ func resourcePostgreSQLRole() *schema.Resource { Optional: true, Description: "Role to switch to at login", }, + roleParameterAttr: { + Type: schema.TypeSet, + Optional: true, + Description: "Configuration parameters", + Elem: resourcePostgreSQLRoleConfigurationParameter(), + }, + }, + } +} + +func resourcePostgreSQLRoleConfigurationParameter() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + roleParameterNameAttr: { + Type: schema.TypeString, + Required: true, + Description: "Name of the configuration parameter to set", + ValidateFunc: validation.StringNotInSlice(ignoredRoleConfigurationParameters, true), + }, + roleParameterValueAttr: { + Type: schema.TypeString, + Required: true, + Description: "Value of the configuration parameter", + }, + roleParameterQuoteAttr: { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Quote the parameter value as a literal", + }, }, } } @@ -311,6 +353,10 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = setConfigurationParameters(txn, d); err != nil { + return err + } + if err = txn.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -480,6 +526,8 @@ func resourcePostgreSQLRoleReadImpl(db *DBConnection, d *schema.ResourceData) er } d.Set(rolePasswordAttr, password) + + d.Set(roleParameterAttr, readRoleParameters(roleConfig, d.Get(roleParameterAttr).(*schema.Set))) return nil } @@ -547,6 +595,28 @@ func readAssumeRole(roleConfig pq.ByteaArray) string { return res } +func readRoleParameters(roleConfig pq.ByteaArray, existingParams *schema.Set) *schema.Set { + params := make([]interface{}, 0) + for _, v := range roleConfig { + tokens := strings.Split(string(v), "=") + if !sliceContainsStr(ignoredRoleConfigurationParameters, tokens[0]) { + quote := true + for _, p := range existingParams.List() { + existingParam := p.(map[string]interface{}) + if existingParam[roleParameterNameAttr].(string) == tokens[0] { + quote = existingParam[roleParameterQuoteAttr].(bool) + } + } + params = append(params, map[string]interface{}{ + roleParameterNameAttr: tokens[0], + roleParameterValueAttr: tokens[1], + roleParameterQuoteAttr: quote, + }) + } + } + return schema.NewSet(schema.HashResource(resourcePostgreSQLRoleConfigurationParameter()), params) +} + // readRolePassword reads password either from Postgres if admin user is a superuser // or only from Terraform state. func readRolePassword(db *DBConnection, d *schema.ResourceData, roleCanLogin bool) (string, error) { @@ -689,6 +759,10 @@ func resourcePostgreSQLRoleUpdate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = setConfigurationParameters(txn, d); err != nil { + return err + } + if err = txn.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } @@ -961,6 +1035,47 @@ func grantRoles(txn *sql.Tx, d *schema.ResourceData) error { return nil } +func setConfigurationParameters(txn *sql.Tx, d *schema.ResourceData) error { + role := d.Get(roleNameAttr).(string) + if d.HasChange(roleParameterAttr) { + o, n := d.GetChange(roleParameterAttr) + oldParams := o.(*schema.Set) + newParams := n.(*schema.Set) + for _, p := range oldParams.List() { + if !newParams.Contains(p) { + param := p.(map[string]interface{}) + query := fmt.Sprintf( + "ALTER ROLE %s RESET %s", + pq.QuoteIdentifier(role), + pq.QuoteIdentifier(param[roleParameterNameAttr].(string))) + log.Printf("[DEBUG] setConfigurationParameters: %s", query) + if _, err := txn.Exec(query); err != nil { + return err + } + } + } + for _, p := range newParams.List() { + if !oldParams.Contains(p) { + param := p.(map[string]interface{}) + value := param[roleParameterValueAttr].(string) + if param[roleParameterQuoteAttr].(bool) { + value = pq.QuoteLiteral(value) + } + query := fmt.Sprintf( + "ALTER ROLE %s SET %s TO %s", + pq.QuoteIdentifier(role), + pq.QuoteIdentifier(param[roleParameterNameAttr].(string)), + value) + log.Printf("[DEBUG] setConfigurationParameters: %s", query) + if _, err := txn.Exec(query); err != nil { + return err + } + } + } + } + return 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 ef502f00..ec43d0a0 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -192,6 +192,184 @@ resource "postgresql_role" "update_role" { }) } +func TestAccPostgresqlRole_ConfigurationParameters(t *testing.T) { + + var configRoles = ` +resource "postgresql_role" "role" { + name = "role1" + %s +} + +resource "postgresql_role" "role_created_with_params" { + name = "role2" + + parameter { + name = "client_min_messages" + value = "debug" + } +} +` + configParameterA := ` + parameter { + name = "client_min_messages" + value = "%s" + } +` + configParameterB := ` + parameter { + name = "maintenance_work_mem" + value = "10000" + quote = false + } +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featurePrivileges) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlRoleDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configRoles, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "0"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "name", "role2"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.#", "1"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.0.value", "debug"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{}), + testAccCheckRoleHasConfigurationParameters("role2", map[string]string{"client_min_messages": "debug"}), + ), + }, + { + Config: fmt.Sprintf(configRoles, fmt.Sprintf(configParameterA, "notice")+configParameterB), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "2"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.value", "notice"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.1.name", "maintenance_work_mem"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.1.value", "10000"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "name", "role2"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.#", "1"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{ + "client_min_messages": "notice", + "maintenance_work_mem": "10000", + }), + ), + }, + { + Config: fmt.Sprintf(configRoles, fmt.Sprintf(configParameterA, "error")), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.value", "error"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "name", "role2"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.#", "1"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{"client_min_messages": "error"}), + ), + }, + { + Config: fmt.Sprintf(configRoles, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "0"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "name", "role2"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.#", "1"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role_created_with_params", "parameter.0.value", "debug"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{}), + testAccCheckRoleHasConfigurationParameters("role2", map[string]string{"client_min_messages": "debug"}), + ), + }, + }, + }) +} + +func TestAccPostgresqlRole_ConfigurationParameters_WithExplicitParameterAttrs(t *testing.T) { + var configRole = ` +resource "postgresql_role" "role" { + name = "role1" + search_path = ["here","there"] + idle_in_transaction_session_timeout = 300 + statement_timeout = 100 + assume_role = "other_role" + %s +} +` + configParameterA := ` + parameter { + name = "client_min_messages" + value = "%s" + } +` + configParameterB := ` + parameter { + name = "maintenance_work_mem" + value = "10000" + quote = false + } +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featurePrivileges) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlRoleDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configRole, fmt.Sprintf(configParameterA, "debug")), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.value", "debug"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{"client_min_messages": "debug"}), + ), + }, + { + Config: fmt.Sprintf(configRole, fmt.Sprintf(configParameterA, "notice")+configParameterB), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "2"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.value", "notice"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.1.name", "maintenance_work_mem"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.1.value", "10000"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{ + "client_min_messages": "notice", + "maintenance_work_mem": "10000", + }), + ), + }, + { + Config: fmt.Sprintf(configRole, fmt.Sprintf(configParameterA, "error")), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.name", "client_min_messages"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.0.value", "error"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{"client_min_messages": "error"}), + ), + }, + { + Config: fmt.Sprintf(configRole, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_role.role", "name", "role1"), + resource.TestCheckResourceAttr("postgresql_role.role", "parameter.#", "0"), + testAccCheckRoleHasConfigurationParameters("role1", map[string]string{}), + ), + }, + }, + }) +} + // Test to create a role with admin user (usually postgres) granted to it // There were a bug on RDS like setup (with a non-superuser postgres role) // where it couldn't delete the role in this case. @@ -295,6 +473,42 @@ func checkRoleExists(client *Client, roleName string) (bool, error) { return true, nil } +func testAccCheckRoleHasConfigurationParameters(roleName string, parameters map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + db, err := client.Connect() + if err != nil { + return err + } + rows, err := db.Query("SELECT UNNEST(rolconfig) FROM pg_roles WHERE rolname=$1", roleName) + if err != nil { + return err + } + setParameters := make(map[string]string) + for rows.Next() { + var param string + if err := rows.Scan(¶m); err != nil { + return err + } + split := strings.Split(param, "=") + if !sliceContainsStr(ignoredRoleConfigurationParameters, split[0]) { + setParameters[split[0]] = split[1] + } + } + if len(parameters) != len(setParameters) { + return fmt.Errorf("expected role %s to have %d configuration parameters, found %d", roleName, len(parameters), len(setParameters)) + } + for k := range parameters { + if parameters[k] != setParameters[k] { + return fmt.Errorf( + "expected configuration parameter %s for role %s to have value \"%s\", found \"%s\"", + k, roleName, parameters[k], setParameters[k]) + } + } + return nil + } +} + func testAccCheckRoleCanLogin(t *testing.T, role, password string) resource.TestCheckFunc { return func(s *terraform.State) error { config := getTestConfig(t) diff --git a/website/docs/r/postgresql_role.html.markdown b/website/docs/r/postgresql_role.html.markdown index 54544893..8c0dbe1b 100644 --- a/website/docs/r/postgresql_role.html.markdown +++ b/website/docs/r/postgresql_role.html.markdown @@ -16,7 +16,7 @@ automatically run a [`REASSIGN OWNED`](https://www.postgresql.org/docs/current/static/sql-reassign-owned.html) and [`DROP OWNED`](https://www.postgresql.org/docs/current/static/sql-drop-owned.html) to -the `CURRENT_USER` (normally the connected user for the provider). If the +the `CURRENT_USER` (normally the connected user for the provider). If the specified PostgreSQL ROLE owns objects in multiple PostgreSQL databases in the same PostgreSQL Cluster, one PostgreSQL provider per database must be created and all but the final ``postgresql_role`` must specify a `skip_drop_role`. @@ -40,6 +40,17 @@ resource "postgresql_role" "my_replication_role" { connection_limit = 5 password = "md5c98cbfeb6a347a47eb8e96cfb4c4b890" } + +resource "postgresql_role" "my_audited_role" { + name = "audited_role" + login = true + password = "mypas" + + parameter { + name = "pgaudit.log" + value = "all" + } +} ``` ## Argument Reference @@ -48,36 +59,36 @@ resource "postgresql_role" "my_replication_role" { server instance where it is configured. * `superuser` - (Optional) Defines whether the role is a "superuser", and - therefore can override all access restrictions within the database. Default + therefore can override all access restrictions within the database. Default value is `false`. * `create_database` - (Optional) Defines a role's ability to execute `CREATE - DATABASE`. Default value is `false`. + DATABASE`. Default value is `false`. * `create_role` - (Optional) Defines a role's ability to execute `CREATE ROLE`. - A role with this privilege can also alter and drop other roles. Default value + A role with this privilege can also alter and drop other roles. Default value is `false`. * `inherit` - (Optional) Defines whether a role "inherits" the privileges of - roles it is a member of. Default value is `true`. + roles it is a member of. Default value is `true`. -* `login` - (Optional) Defines whether role is allowed to log in. Roles without +* `login` - (Optional) Defines whether role is allowed to log in. Roles without this attribute are useful for managing database privileges, but are not users - in the usual sense of the word. Default value is `false`. + in the usual sense of the word. Default value is `false`. * `replication` - (Optional) Defines whether a role is allowed to initiate - streaming replication or put the system in and out of backup mode. Default + streaming replication or put the system in and out of backup mode. Default value is `false` * `bypass_row_level_security` - (Optional) Defines whether a role bypasses every - row-level security (RLS) policy. Default value is `false`. + row-level security (RLS) policy. Default value is `false`. * `connection_limit` - (Optional) If this role can log in, this specifies how many concurrent connections the role can establish. `-1` (the default) means no limit. * `encrypted_password` - (Optional) Defines whether the password is stored - encrypted in the system catalogs. Default value is `true`. NOTE: this value + encrypted in the system catalogs. Default value is `true`. NOTE: this value is always set (to the conservative and safe value), but may interfere with the behavior of [PostgreSQL's `password_encryption` setting](https://www.postgresql.org/docs/current/static/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION). @@ -92,16 +103,16 @@ resource "postgresql_role" "my_replication_role" { `", "`. * `valid_until` - (Optional) Defines the date and time after which the role's - password is no longer valid. Established connections past this `valid_time` - will have to be manually terminated. This value corresponds to a PostgreSQL + password is no longer valid. Established connections past this `valid_time` + will have to be manually terminated. This value corresponds to a PostgreSQL datetime. If omitted or the magic value `NULL` is used, `valid_until` will be - set to `infinity`. Default is `NULL`, therefore `infinity`. + set to `infinity`. Default is `NULL`, therefore `infinity`. * `skip_drop_role` - (Optional) When a PostgreSQL ROLE exists in multiple databases and the ROLE is dropped, the [cleanup of ownership of objects](https://www.postgresql.org/docs/current/static/role-removal.html) in each of the respective databases must occur before the ROLE can be dropped - from the catalog. Set this option to true when there are multiple databases + from the catalog. Set this option to true when there are multiple databases in a PostgreSQL cluster using the same PostgreSQL ROLE for object ownership. This is the third and final step taken when removing a ROLE from a database. @@ -109,18 +120,37 @@ resource "postgresql_role" "my_replication_role" { databases and the ROLE is dropped, a [`REASSIGN OWNED`](https://www.postgresql.org/docs/current/static/sql-reassign-owned.html) in must be executed on each of the respective databases before the `DROP ROLE` - can be executed to dropped the ROLE from the catalog. This is the first and + can be executed to dropped the ROLE from the catalog. This is the first and second steps taken when removing a ROLE from a database (the second step being an implicit [`DROP OWNED`](https://www.postgresql.org/docs/current/static/sql-drop-owned.html)). -* `statement_timeout` - (Optional) Defines [`statement_timeout`](https://www.postgresql.org/docs/current/runtime-config-client.html#RUNTIME-CONFIG-CLIENT-STATEMENT) setting for this role which allows to abort any statement that takes more than the specified amount of time. +* `statement_timeout` - (Optional) + Defines [`statement_timeout`](https://www.postgresql.org/docs/current/runtime-config-client.html#RUNTIME-CONFIG-CLIENT-STATEMENT) + setting for this role which allows to abort any statement that takes more than the specified amount of time. + +* `assume_role` - (Optional) Defines the role to switch to at login + via [`SET ROLE`](https://www.postgresql.org/docs/current/sql-set-role.html). + +* `parameter` - (Optional) A list of configuration parameter objects. Their keys are documented below. + +### parameter Argument Reference + +~> **NOTE:** Configuration parameters that can be defined as explicit arguments +cannot be declared using `parameter`. You must use the `search_path`, `statement_timeout`, +`idle_in_transaction_session_timeout`, and `assume_role` arguments to set those +configuration parameters. + +* `name` - (Required) Name of a configuration parameter. + +* `value` - (Required) Value to set for the configuration parameter. -* `assume_role` - (Optional) Defines the role to switch to at login via [`SET ROLE`](https://www.postgresql.org/docs/current/sql-set-role.html). +* `quote` - (Optional) Quote the value of the parameter as a literal. + Defaults value is `true`. ## Import Example -`postgresql_role` supports importing resources. Supposing the following +`postgresql_role` supports importing resources. Supposing the following Terraform: ```hcl