diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index c85b44d0b..6dcd0c009 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -104,14 +104,6 @@ func resourcePostgreSQLDatabase() *schema.Resource { } } -func validateConnLimit(v interface{}, key string) (warnings []string, errors []error) { - value := v.(int) - if value < -1 { - errors = append(errors, fmt.Errorf("%d can not be less than -1", key)) - } - return -} - func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error { c := meta.(*Client) conn, err := c.Connect() diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index 89137fab8..dc4191871 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -3,64 +3,232 @@ package postgresql import ( "database/sql" "fmt" + "log" + "strconv" + "strings" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/schema" "github.com/lib/pq" ) +const ( + roleBypassRLSAttr = "bypass_row_level_security" + roleConnLimitAttr = "connection_limit" + roleCreateDBAttr = "create_database" + roleCreateRoleAttr = "create_role" + roleEncryptedPassAttr = "encrypted_password" + roleInheritAttr = "inherit" + roleLoginAttr = "login" + roleNameAttr = "name" + rolePasswordAttr = "password" + roleReplicationAttr = "replication" + roleSuperUserAttr = "superuser" + roleValidUntilAttr = "valid_until" + + // Deprecated options + roleDepEncryptedAttr = "encrypted" +) + func resourcePostgreSQLRole() *schema.Resource { return &schema.Resource{ Create: resourcePostgreSQLRoleCreate, Read: resourcePostgreSQLRoleRead, Update: resourcePostgreSQLRoleUpdate, Delete: resourcePostgreSQLRoleDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + roleNameAttr: { + Type: schema.TypeString, + Required: true, + Description: "The name of the role", }, - "login": { - Type: schema.TypeBool, - Optional: true, - ForceNew: false, - Default: false, + rolePasswordAttr: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil), + Description: "Sets the role's password", }, - "password": { - Type: schema.TypeString, - Optional: true, - ForceNew: false, + roleDepEncryptedAttr: { + Type: schema.TypeString, + Optional: true, + Deprecated: fmt.Sprintf("Rename PostgreSQL role resource attribute %q to %q", roleDepEncryptedAttr, roleEncryptedPassAttr), }, - "encrypted": { - Type: schema.TypeBool, - Optional: true, - ForceNew: false, - Default: false, + roleEncryptedPassAttr: { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Control whether the password is stored encrypted in the system catalogs", + }, + + roleValidUntilAttr: { + Type: schema.TypeString, + Optional: true, + Description: "Sets a date and time after which the role's password is no longer valid", + }, + roleConnLimitAttr: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "How many concurrent connections can be made with this role", + ValidateFunc: validateConnLimit, + }, + roleSuperUserAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: `Determine whether the new role is a "superuser"`, + }, + roleCreateDBAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Define a role's ability to create databases", + }, + roleCreateRoleAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Determine whether this role will be permitted to create new roles", + }, + roleInheritAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: `Determine whether a role "inherits" the privileges of roles it is a member of`, + }, + roleLoginAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Determine whether a role is allowed to log in", + }, + roleReplicationAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Determine whether a role is allowed to initiate streaming replication or put the system in and out of backup mode", + }, + roleBypassRLSAttr: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Determine whether a role bypasses every row-level security (RLS) policy", }, }, } } func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*Client) - conn, err := client.Connect() + c := meta.(*Client) + conn, err := c.Connect() if err != nil { - return err + return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err) } defer conn.Close() - roleName := d.Get("name").(string) - loginAttr := getLoginStr(d.Get("login").(bool)) - password := d.Get("password").(string) + stringOpts := []struct { + hclKey string + sqlKey string + }{ + {rolePasswordAttr, "PASSWORD"}, + {roleValidUntilAttr, "VALID UNTIL"}, + } + intOpts := []struct { + hclKey string + sqlKey string + }{ + {roleConnLimitAttr, "CONNECTION LIMIT"}, + } + boolOpts := []struct { + hclKey string + sqlKeyEnable string + sqlKeyDisable string + }{ + {roleSuperUserAttr, "CREATEDB", "NOCREATEDB"}, + {roleCreateRoleAttr, "CREATEROLE", "NOCREATEROLE"}, + {roleInheritAttr, "INHERIT", "NOINHERIT"}, + {roleLoginAttr, "LOGIN", "NOLOGIN"}, + {roleReplicationAttr, "REPLICATION", "NOREPLICATION"}, + {roleBypassRLSAttr, "BYPASSRLS", "NOBYPASSRLS"}, - encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool)) + // roleEncryptedPassAttr is used only when rolePasswordAttr is set. + // {roleEncryptedPassAttr, "ENCRYPTED", "UNENCRYPTED"}, + } - query := fmt.Sprintf("CREATE ROLE %s %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), loginAttr, encryptedCfg, password) + createOpts := make([]string, 0, len(stringOpts)+len(intOpts)+len(boolOpts)) + + for _, opt := range stringOpts { + v, ok := d.GetOk(opt.hclKey) + if !ok { + continue + } + + val := v.(string) + if val != "" { + switch { + case opt.hclKey == rolePasswordAttr: + if strings.ToUpper(v.(string)) == "NULL" { + createOpts = append(createOpts, "PASSWORD NULL") + } else { + if d.Get(roleEncryptedPassAttr).(bool) { + createOpts = append(createOpts, "ENCRYPTED") + } else { + createOpts = append(createOpts, "UNENCRYPTED") + } + escapedPassword := strconv.Quote(val) + escapedPassword = strings.TrimLeft(escapedPassword, `"`) + escapedPassword = strings.TrimRight(escapedPassword, `"`) + createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, escapedPassword)) + } + case opt.hclKey == roleValidUntilAttr: + switch { + case v.(string) == "", strings.ToUpper(v.(string)) == "NULL": + createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, "'infinity'")) + default: + createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, pq.QuoteIdentifier(val))) + } + default: + createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, pq.QuoteIdentifier(val))) + } + } + } + + for _, opt := range intOpts { + val := d.Get(opt.hclKey).(int) + createOpts = append(createOpts, fmt.Sprintf("%s %d", opt.sqlKey, val)) + } + + for _, opt := range boolOpts { + if opt.hclKey == roleEncryptedPassAttr { + // This attribute is handled above in the stringOpts + // loop. + continue + } + val := d.Get(opt.hclKey).(bool) + + valStr := opt.sqlKeyDisable + if val { + valStr = opt.sqlKeyEnable + } + createOpts = append(createOpts, valStr) + } + + roleName := d.Get(roleNameAttr).(string) + createStr := strings.Join(createOpts, " ") + if len(createOpts) > 0 { + createStr = " WITH " + createStr + } + + query := fmt.Sprintf("CREATE ROLE %s%s", pq.QuoteIdentifier(roleName), createStr) _, err = conn.Query(query) if err != nil { - return errwrap.Wrapf("Error creating role: {{err}}", err) + return errwrap.Wrapf(fmt.Sprintf("Error creating role %s: {{err}}", roleName), err) } d.SetId(roleName) @@ -76,7 +244,7 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro } defer conn.Close() - roleName := d.Get("name").(string) + roleName := d.Get(roleNameAttr).(string) query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName)) _, err = conn.Query(query) @@ -90,25 +258,32 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro } func resourcePostgreSQLRoleRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*Client) - conn, err := client.Connect() + c := meta.(*Client) + conn, err := c.Connect() if err != nil { return err } defer conn.Close() - roleName := d.Get("name").(string) + roleName := d.Get(roleNameAttr).(string) + if roleName == "" { + roleName = d.Id() + } - var canLogin bool - err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&canLogin) + var roleCanLogin bool + err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&roleCanLogin) switch { case err == sql.ErrNoRows: + log.Printf("[WARN] PostgreSQL database (%s) not found", d.Id()) d.SetId("") return nil case err != nil: return errwrap.Wrapf("Error reading role: {{err}}", err) default: - d.Set("login", canLogin) + d.Set(roleNameAttr, roleName) + d.Set(roleLoginAttr, roleCanLogin) + d.Set("encrypted", true) + d.SetId(roleName) return nil } } @@ -123,21 +298,21 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro d.Partial(true) - roleName := d.Get("name").(string) + roleName := d.Get(roleNameAttr).(string) - if d.HasChange("login") { - loginAttr := getLoginStr(d.Get("login").(bool)) + if d.HasChange(roleLoginAttr) { + loginAttr := getLoginStr(d.Get(roleLoginAttr).(bool)) query := fmt.Sprintf("ALTER ROLE %s %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(loginAttr)) _, err := conn.Query(query) if err != nil { return errwrap.Wrapf("Error updating login attribute for role: {{err}}", err) } - d.SetPartial("login") + d.SetPartial(roleLoginAttr) } - password := d.Get("password").(string) - if d.HasChange("password") { + password := d.Get(rolePasswordAttr).(string) + if d.HasChange(rolePasswordAttr) { encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool)) query := fmt.Sprintf("ALTER ROLE %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), encryptedCfg, password) @@ -146,7 +321,7 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro return errwrap.Wrapf("Error updating password attribute for role: {{err}}", err) } - d.SetPartial("password") + d.SetPartial(rolePasswordAttr) } if d.HasChange("encrypted") { diff --git a/builtin/providers/postgresql/resource_postgresql_role_test.go b/builtin/providers/postgresql/resource_postgresql_role_test.go index 2188a1ef7..31d0b0ebe 100644 --- a/builtin/providers/postgresql/resource_postgresql_role_test.go +++ b/builtin/providers/postgresql/resource_postgresql_role_test.go @@ -24,6 +24,29 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { "postgresql_role.myrole2", "name", "myrole2"), resource.TestCheckResourceAttr( "postgresql_role.myrole2", "login", "true"), + + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "name", "testing_role_with_defaults"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "superuser", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "create_database", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "create_role", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "inherit", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "replication", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "bypass_row_level_security", "false"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "connection_limit", "-1"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "encrypted_password", "true"), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "password", ""), + resource.TestCheckResourceAttr( + "postgresql_role.role_with_defaults", "valid_until", "NULL"), ), }, }, @@ -129,4 +152,19 @@ resource "postgresql_role" "role_with_pwd_no_login" { resource "postgresql_role" "role_simple" { name = "role_simple" } + +resource "postgresql_role" "role_with_defaults" { + name = "testing_role_with_defaults" + superuser = false + create_database = false + create_role = false + inherit = false + login = false + replication = false + bypass_row_level_security = false + connection_limit = -1 + encrypted_password = true + password = "" + valid_until = "NULL" +} ` diff --git a/builtin/providers/postgresql/validators.go b/builtin/providers/postgresql/validators.go new file mode 100644 index 000000000..8bc75209e --- /dev/null +++ b/builtin/providers/postgresql/validators.go @@ -0,0 +1,11 @@ +package postgresql + +import "fmt" + +func validateConnLimit(v interface{}, key string) (warnings []string, errors []error) { + value := v.(int) + if value < -1 { + errors = append(errors, fmt.Errorf("%d can not be less than -1", key)) + } + return +} diff --git a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown index 6d765f8ef..35d5b5815 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown @@ -42,8 +42,8 @@ resource "postgresql_database" "my_db" { tablespace. This tablespace will be the default tablespace used for objects created in this database. -* `connection_limit` - (Optional) How many concurrent connections can be made to - this database. `-1` (the default) means no limit. +* `connection_limit` - (Optional) How many concurrent connections can be + established to this database. `-1` (the default) means no limit. * `allow_connections` - (Optional) If `false` then no one can connect to this database. The default is `true`, allowing connections (except as restricted by @@ -95,7 +95,7 @@ provider "postgresql" { resource "postgresql_database" "db1" { provider = "postgresql.admindb" - name = "db1" + name = "testdb1" } ``` @@ -103,5 +103,9 @@ It is possible to import a `postgresql_database` resource with the following command: ``` -$ terraform import postgresql_database.testdb1 testdb1 +$ terraform import postgresql_database.db1 testdb1 ``` + +Where `testdb1` is the name of the database to import and +`postgresql_database.db1` is the name of the resource whose state will be +populated as a result of the command. diff --git a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown index a5d5c17d8..e2818dc80 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown @@ -19,19 +19,93 @@ resource "postgresql_role" "my_role" { name = "my_role" login = true password = "mypass" - encrypted = true } +resource "postgresql_role" "my_replication_role" { + name = "replication_role" + replication = true + login = true + connection_limit = 5 + password = "md5c98cbfeb6a347a47eb8e96cfb4c4b890" +} ``` ## Argument Reference -* `name` - (Required) The name of the role. Must be unique on the PostgreSQL server instance - where it is configured. +* `name` - (Required) The name of the role. Must be unique on the PostgreSQL + server instance where it is configured. -* `login` - (Optional) Configures whether a role is allowed to log in; that is, whether the role can be given as the initial session authorization name during client connection. Corresponds to the LOGIN/NOLOGIN -clauses in 'CREATE ROLE'. Default value is false. +* `superuser` - (Optional) Defines whether the role is a "superuser", and + therefore can override all access restrictions within the database. Default + value is `false`. -* `password` - (Optional) Sets the role's password. (A password is only of use for roles having the LOGIN attribute, but you can nonetheless define one for roles without it.) If you do not plan to use password authentication you can omit this option. If no password is specified, the password will be set to null and password authentication will always fail for that user. +* `create_database` - (Optional) Defines a role's ability to execute `CREATE + DATABASE`. Default value is `false`. -* `encrypted` - (Optional) Corresponds to ENCRYPTED, UNENCRYPTED in PostgreSQL. This controls whether the password is stored encrypted in the system catalogs. Default is false. \ No newline at end of file +* `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 + is `false`. + +* `inherit` - (Optional) Defines whether a role "inherits" the privileges of + roles it is a member of. Default value is `false`. + +* `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`. + +* `replication` - (Optional) Defines whether a role is allowed to initiate + 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`. + +* `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 + 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). + +* `password` - (Optional) Sets the role's password. (A password is only of use + for roles having the `login` attribute set to true, but you can nonetheless + define one for roles without it.) Roles without a password explicitly set are + left alone. If the password is set to the magic value `NULL`, the password + will be always be cleared. + +* `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 + datetime. If omitted or the magic value `NULL` is used, `valid_until` will be + set to `infinity`. Default is `NULL`, therefore `infinity`. + +## Import Example + +`postgresql_role` supports importing resources. Supposing the following +Terraform: + +``` +provider "postgresql" { + alias = "admindb" +} + +resource "postgresql_role" "replication_role" { + provider = "postgresql.admindb" + + name = "replication_name" +} +``` + +It is possible to import a `postgresql_role` resource with the following +command: + +``` +$ terraform import postgresql_role.replication_role replication_name +``` + +Where `replication_name` is the name of the role to import and +`postgresql_role.replication_role` is the name of the resource whose state will +be populated as a result of the command.