diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..9776db75 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +# This GitHub action can publish assets for release when a tag is created. +# Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). +# +# This uses an action (paultyng/ghaction-import-gpg) that assumes you set your +# private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` +# secret. If you would rather own your own GPG handling, please fork this action +# or use an alternative one for key handling. +# +# You will need to pass the `--batch` flag to `gpg` in your signing step +# in `goreleaser` to indicate this is being used in a non-interactive mode. +# +name: release +on: + push: + tags: + - 'v*' +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Unshallow + run: git fetch --prune --unshallow + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.14 + - + name: Import GPG key + id: import_gpg + uses: paultyng/ghaction-import-gpg@v2.1.0 + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + PASSPHRASE: ${{ secrets.PASSPHRASE }} + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: latest + args: release --rm-dist + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.travis.yml b/.travis.yml index aca93180..8e3959c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,6 @@ install: script: - make test - make vet -- make website-test - make testacc branches: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf4f1c5..6e684410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,45 @@ -## 1.8.0 (Unreleased) -## 1.7.2 (unreleased) + +## 1.9.0 (Unreleased) + +FEATURES: +* `postgresql_grant_role`: Non-authoritative. Grant role to another role. + +## 1.8.1 (November 26, 2020) BUG FIXES: -* `postgresql_grant` : fix grant on function by removing prokind column selection, - as is it not available on postgresql version < 11 and its not to - expect to have a window(w) or aggregate function(a) with the same name as a normal function(f) - +* Revert "Use lazy connections" [#199](https://github.com/terraform-providers/terraform-provider-postgresql/pull/199) + Plugin panics if not able to connect to the database. + +## 1.8.0 (November 26, 2020) + +FEATURES: + +* `postgresql_extension`: Support drop cascade. + ([#162](https://github.com/terraform-providers/terraform-provider-postgresql/pull/162) - @multani) + +* ~~Use lazy connections. + ([#199](https://github.com/terraform-providers/terraform-provider-postgresql/pull/199) - @estahn)~~ (Reverted in 1.8.1) + +BUG FIXES: + +* `postgresql_grant`: Fix grant on function by removing `prokind` column selection. + ([#171](https://github.com/terraform-providers/terraform-provider-postgresql/pull/171) - @Tommi2Day) + +DEV IMPROVEMENTS: + +* Set up Github Workflows to create releases. + ([#3](https://github.com/cyrilgdn/terraform-provider-postgresql/pull/3) - @thenonameguy) + +## 1.7.2 (July 30, 2020) + +This is the first release on [Terraform registry](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest) + +DEV IMPROVEMENTS: + +* Add goreleaser config +* Pusblish on Terraform registry: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest + ## 1.7.1 (July 30, 2020) BUG FIXES: diff --git a/README.md b/README.md index d71cb0b6..3448d17a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -Terraform Provider -================== +Terraform Provider for PostgreSQL +================================= -- Website: https://www.terraform.io -- [![Gitter chat](https://badges.gitter.im/hashicorp-terraform/Lobby.png)](https://gitter.im/hashicorp-terraform/Lobby) -- Mailing list: [Google Groups](http://groups.google.com/group/terraform-tool) +This provider allows to manage with Terraform [Postgresql](https://www.postgresql.org/) objects like databases, extensions, roles, etc.. - +It's published on the [Terraform registry](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs). +It replaces https://github.com/hashicorp/terraform-provider-postgresql since Hashicorp stopped hosting community providers in favor of the Terraform registry. + +- Documentation: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs Requirements ------------ -- [Terraform](https://www.terraform.io/downloads.html) 0.10.x -- [Go](https://golang.org/doc/install) 1.11 (to build the provider plugin) +- [Terraform](https://www.terraform.io/downloads.html) 0.12.x +- [Go](https://golang.org/doc/install) 1.14 (to build the provider plugin) Building The Provider --------------------- diff --git a/postgresql/config.go b/postgresql/config.go index aae9c9d1..371831b9 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -119,9 +119,43 @@ func (c *Config) NewClient(database string) (*Client, error) { dbRegistryLock.Lock() defer dbRegistryLock.Unlock() + dsn := c.connStr(database) + dbEntry, found := dbRegistry[dsn] + if !found { + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, fmt.Errorf("Error connecting to PostgreSQL server: %w", err) + } + + // We don't want to retain connection + // So when we connect on a specific database which might be managed by terraform, + // we don't keep opened connection in case of the db has to be dopped in the plan. + db.SetMaxIdleConns(0) + db.SetMaxOpenConns(c.MaxConns) + + defaultVersion, _ := semver.Parse(defaultExpectedPostgreSQLVersion) + version := &c.ExpectedVersion + if defaultVersion.Equals(c.ExpectedVersion) { + // Version hint not set by user, need to fingerprint + version, err = fingerprintCapabilities(db) + if err != nil { + db.Close() + return nil, fmt.Errorf("error detecting capabilities: %w", err) + } + } + + dbEntry = dbRegistryEntry{ + db: db, + version: *version, + } + dbRegistry[dsn] = dbEntry + } + client := Client{ config: *c, databaseName: database, + db: dbEntry.db, + version: dbEntry.version, } return &client, nil @@ -271,52 +305,9 @@ func (c *Config) getDatabaseUsername() string { // return their database resources. Use of QueryRow() or Exec() is encouraged. // Query() must have their rows.Close()'ed. func (c *Client) DB() *sql.DB { - c.connectDB() return c.db } -func (c *Client) connectDB() (*Client, error) { - dbRegistryLock.Lock() - defer dbRegistryLock.Unlock() - - dsn := c.config.connStr(c.databaseName) - dbEntry, found := dbRegistry[dsn] - if !found { - db, err := sql.Open("postgres", dsn) - if err != nil { - return nil, fmt.Errorf("Error connecting to PostgreSQL server: %w", err) - } - - // We don't want to retain connection - // So when we connect on a specific database which might be managed by terraform, - // we don't keep opened connection in case of the db has to be dopped in the plan. - db.SetMaxIdleConns(0) - db.SetMaxOpenConns(c.config.MaxConns) - - defaultVersion, _ := semver.Parse(defaultExpectedPostgreSQLVersion) - version := &c.config.ExpectedVersion - if defaultVersion.Equals(c.config.ExpectedVersion) { - // Version hint not set by user, need to fingerprint - version, err = fingerprintCapabilities(db) - if err != nil { - db.Close() - return nil, fmt.Errorf("error detecting capabilities: %w", err) - } - } - - dbEntry = dbRegistryEntry{ - db: db, - version: *version, - } - dbRegistry[dsn] = dbEntry - } - - c.db = dbEntry.db - c.version = dbEntry.version - - return nil, nil -} - // fingerprintCapabilities queries PostgreSQL to populate a local catalog of // capabilities. This is only run once per Client. func fingerprintCapabilities(db *sql.DB) (*semver.Version, error) { diff --git a/postgresql/provider.go b/postgresql/provider.go index 6eb8c21e..3cf3b89b 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -130,6 +130,7 @@ func Provider() terraform.ResourceProvider { "postgresql_default_privileges": resourcePostgreSQLDefaultPrivileges(), "postgresql_extension": resourcePostgreSQLExtension(), "postgresql_grant": resourcePostgreSQLGrant(), + "postgresql_grant_role": resourcePostgreSQLGrantRole(), "postgresql_schema": resourcePostgreSQLSchema(), "postgresql_role": resourcePostgreSQLRole(), }, diff --git a/postgresql/resource_postgresql_grant_role.go b/postgresql/resource_postgresql_grant_role.go new file mode 100644 index 00000000..902a4621 --- /dev/null +++ b/postgresql/resource_postgresql_grant_role.go @@ -0,0 +1,218 @@ +package postgresql + +import ( + "database/sql" + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/lib/pq" +) + +const ( + // This returns the role membership for role, grant_role + getGrantRoleQuery = ` +SELECT + pg_get_userbyid(member) as role, + pg_get_userbyid(roleid) as grant_role, + admin_option +FROM + pg_auth_members +WHERE + pg_get_userbyid(member) = $1 AND + pg_get_userbyid(roleid) = $2; +` +) + +func resourcePostgreSQLGrantRole() *schema.Resource { + return &schema.Resource{ + Create: resourcePostgreSQLGrantRoleCreate, + Read: resourcePostgreSQLGrantRoleRead, + Delete: resourcePostgreSQLGrantRoleDelete, + + Schema: map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the role to grant grant_role", + }, + "grant_role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the role that is granted to role", + }, + "with_admin_option": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + Description: "Permit the grant recipient to grant it to others", + }, + }, + } +} + +func resourcePostgreSQLGrantRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + if !client.featureSupported(featurePrivileges) { + return fmt.Errorf( + "postgresql_grant_role resource is not supported for this Postgres version (%s)", + client.version, + ) + } + + client.catalogLock.RLock() + defer client.catalogLock.RUnlock() + + return readGrantRole(client.DB(), d) +} + +func resourcePostgreSQLGrantRoleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + if !client.featureSupported(featurePrivileges) { + return fmt.Errorf( + "postgresql_grant_role resource is not supported for this Postgres version (%s)", + client.version, + ) + } + + client.catalogLock.Lock() + defer client.catalogLock.Unlock() + + txn, err := startTransaction(client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + // Revoke the granted roles before granting them again. + if err = revokeRole(txn, d); err != nil { + return err + } + + if err = grantRole(txn, d); err != nil { + return err + } + + if err = txn.Commit(); err != nil { + return fmt.Errorf("could not commit transaction: %w", err) + } + + d.SetId(generateGrantRoleID(d)) + + return readGrantRole(client.DB(), d) +} + +func resourcePostgreSQLGrantRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + if !client.featureSupported(featurePrivileges) { + return fmt.Errorf( + "postgresql_grant_role resource is not supported for this Postgres version (%s)", + client.version, + ) + } + + client.catalogLock.Lock() + defer client.catalogLock.Unlock() + + txn, err := startTransaction(client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + if err = revokeRole(txn, d); err != nil { + return err + } + + if err = txn.Commit(); err != nil { + return fmt.Errorf("could not commit transaction: %w", err) + } + + return nil +} + +func readGrantRole(db QueryAble, d *schema.ResourceData) error { + var roleName, grantRoleName string + var withAdminOption bool + + grantRoleID := d.Id() + + values := []interface{}{ + &roleName, + &grantRoleName, + &withAdminOption, + } + + err := db.QueryRow(getGrantRoleQuery, d.Get("role"), d.Get("grant_role")).Scan(values...) + switch { + case err == sql.ErrNoRows: + log.Printf("[WARN] PostgreSQL grant role (%q) not found", grantRoleID) + d.SetId("") + return nil + case err != nil: + return fmt.Errorf("Error reading grant role: %w", err) + } + + d.Set("role", roleName) + d.Set("grant_role", grantRoleName) + d.Set("with_admin_option", withAdminOption) + + d.SetId(generateGrantRoleID(d)) + + return nil +} + +func createGrantRoleQuery(d *schema.ResourceData) string { + grantRole, _ := d.Get("grant_role").(string) + role, _ := d.Get("role").(string) + + query := fmt.Sprintf( + "GRANT %s TO %s", + pq.QuoteIdentifier(grantRole), + pq.QuoteIdentifier(role), + ) + if wao, _ := d.Get("with_admin_option").(bool); wao { + query = query + " WITH ADMIN OPTION" + } + + return query +} + +func createRevokeRoleQuery(d *schema.ResourceData) string { + grantRole, _ := d.Get("grant_role").(string) + role, _ := d.Get("role").(string) + + return fmt.Sprintf( + "REVOKE %s FROM %s", + pq.QuoteIdentifier(grantRole), + pq.QuoteIdentifier(role), + ) +} + +func grantRole(txn *sql.Tx, d *schema.ResourceData) error { + query := createGrantRoleQuery(d) + if _, err := txn.Exec(query); err != nil { + return fmt.Errorf("could not execute grant query: %w", err) + } + return nil +} + +func revokeRole(txn *sql.Tx, d *schema.ResourceData) error { + query := createRevokeRoleQuery(d) + if _, err := txn.Exec(query); err != nil { + return fmt.Errorf("could not execute revoke query: %w", err) + } + return nil +} + +func generateGrantRoleID(d *schema.ResourceData) string { + return strings.Join([]string{d.Get("role").(string), d.Get("grant_role").(string), strconv.FormatBool(d.Get("with_admin_option").(bool))}, "_") +} diff --git a/postgresql/resource_postgresql_grant_role_test.go b/postgresql/resource_postgresql_grant_role_test.go new file mode 100644 index 00000000..3f444f8c --- /dev/null +++ b/postgresql/resource_postgresql_grant_role_test.go @@ -0,0 +1,172 @@ +package postgresql + +import ( + "database/sql" + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/lib/pq" +) + +func TestCreateGrantRoleQuery(t *testing.T) { + var roleName = "foo" + var grantRoleName = "bar" + + cases := []struct { + resource map[string]interface{} + expected string + }{ + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + }, + expected: fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), + }, + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + "with_admin_option": false, + }, + expected: fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), + }, + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + "with_admin_option": true, + }, + expected: fmt.Sprintf("GRANT %s TO %s WITH ADMIN OPTION", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)), + }, + } + + for _, c := range cases { + out := createGrantRoleQuery(schema.TestResourceDataRaw(t, resourcePostgreSQLGrantRole().Schema, c.resource)) + if out != c.expected { + t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected) + } + } +} + +func TestRevokeRoleQuery(t *testing.T) { + var roleName = "foo" + var grantRoleName = "bar" + + expected := fmt.Sprintf("REVOKE %s FROM %s", pq.QuoteIdentifier(grantRoleName), pq.QuoteIdentifier(roleName)) + + cases := []struct { + resource map[string]interface{} + }{ + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + }, + }, + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + "with_admin_option": false, + }, + }, + { + resource: map[string]interface{}{ + "role": roleName, + "grant_role": grantRoleName, + "with_admin_option": true, + }, + }, + } + + for _, c := range cases { + out := createRevokeRoleQuery(schema.TestResourceDataRaw(t, resourcePostgreSQLGrantRole().Schema, c.resource)) + if out != expected { + t.Fatalf("Error matching output and expected: %#v vs %#v", out, expected) + } + } +} + +func TestAccPostgresqlGrantRole(t *testing.T) { + skipIfNotAcc(t) + + config := getTestConfig(t) + dsn := config.connStr("postgres") + + dbSuffix, teardown := setupTestDatabase(t, false, true) + defer teardown() + + _, roleName := getTestDBNames(dbSuffix) + + grantedRoleName := "foo" + + testAccPostgresqlGrantRoleResources := fmt.Sprintf(` + resource postgresql_role "grant" { + name = "%s" + } + resource postgresql_grant_role "grant_role" { + role = "%s" + grant_role = postgresql_role.grant.name + with_admin_option = true + } + `, grantedRoleName, roleName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featurePrivileges) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccPostgresqlGrantRoleResources, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "postgresql_grant_role.grant_role", "role", roleName), + resource.TestCheckResourceAttr( + "postgresql_grant_role.grant_role", "grant_role", grantedRoleName), + resource.TestCheckResourceAttr( + "postgresql_grant_role.grant_role", "with_admin_option", strconv.FormatBool(true)), + checkGrantRole(t, dsn, roleName, grantedRoleName, true), + ), + }, + }, + }) +} + +func checkGrantRole(t *testing.T, dsn, role string, grantRole string, withAdmin bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + db, err := sql.Open("postgres", dsn) + if err != nil { + t.Fatalf("could to create connection pool: %v", err) + } + defer db.Close() + + var _rez int + err = db.QueryRow(` + SELECT 1 + FROM pg_auth_members + WHERE pg_get_userbyid(member) = $1 + AND pg_get_userbyid(roleid) = $2 + AND admin_option = $3; + `, role, grantRole, withAdmin).Scan(&_rez) + + switch { + case err == sql.ErrNoRows: + return fmt.Errorf( + "Role %s is not a member of %s", + role, grantRole, + ) + + case err != nil: + t.Fatalf("could not check granted role: %v", err) + } + + return nil + } +} diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index ecb4a43d..20d5a083 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -473,6 +473,9 @@ func readSearchPath(roleConfig pq.ByteaArray) []string { config := string(v) if strings.HasPrefix(config, roleSearchPathAttr) { var result = strings.Split(strings.TrimPrefix(config, roleSearchPathAttr+"="), ", ") + for i := range result { + result[i] = strings.Trim(result[i], `"`) + } return result } } diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index ac299ac8..b8d4f1f4 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -53,7 +53,7 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { 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-with-hyphen"}), // The int part in the attr name is the schema.HashString of the value. resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.719783566", "myrole2"), @@ -346,6 +346,9 @@ func checkSearchPath(client *Client, roleName string, expectedSearchPath []strin } searchPath := strings.Split(searchPathStr, ", ") + for i := range searchPath { + searchPath[i] = strings.Trim(searchPath[i], `"`) + } sort.Strings(expectedSearchPath) if !reflect.DeepEqual(searchPath, expectedSearchPath) { return fmt.Errorf( @@ -411,6 +414,6 @@ resource "postgresql_role" "sub_role" { resource "postgresql_role" "role_with_search_path" { name = "role_with_search_path" - search_path = ["bar", "foo"] + search_path = ["bar", "foo-with-hyphen"] } ` diff --git a/postgresql/utils_test.go b/postgresql/utils_test.go index 38d06ff9..0e6a7e52 100644 --- a/postgresql/utils_test.go +++ b/postgresql/utils_test.go @@ -114,6 +114,21 @@ func setupTestDatabase(t *testing.T, createDB, createRole bool) (string, func()) } } +// createTestRole creates a role before executing a terraform test +// and provides the teardown function to delete all these resources. +func createTestRole(t *testing.T, roleName string) func() { + config := getTestConfig(t) + + dbExecute(t, config.connStr("postgres"), fmt.Sprintf( + "CREATE ROLE %s LOGIN ENCRYPTED PASSWORD '%s'", + roleName, testRolePassword, + )) + + return func() { + dbExecute(t, config.connStr("postgres"), fmt.Sprintf("DROP ROLE IF EXISTS %s", roleName)) + } +} + func createTestTables(t *testing.T, dbSuffix string, tables []string, owner string) func() { config := getTestConfig(t) dbName, _ := getTestDBNames(dbSuffix) diff --git a/website/docs/r/postgresql_grant_role.html.markdown b/website/docs/r/postgresql_grant_role.html.markdown new file mode 100644 index 00000000..c06e5cc9 --- /dev/null +++ b/website/docs/r/postgresql_grant_role.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "postgresql" +page_title: "PostgreSQL: postgresql_grant_role" +sidebar_current: "docs-postgresql-resource-postgresql_grant_role" +description: |- + Creates and manages membership in a role to one or more other roles. +--- + +# postgresql\_grant\_role + +The ``postgresql_grant_role`` resource creates and manages membership in a role to one or more other roles in a non-authoritative way. + +When using ``postgresql_grant_role`` resource it is likely because the PostgreSQL role you are modifying was created outside of this provider. + +~> **Note:** This resource needs PostgreSQL version 9 or above. + +~> **Note:** `postgresql_grant_role` **cannot** be used in conjunction with `postgresql_role` or they will fight over what your role grants should be. + +## Usage + +```hcl +resource "postgresql_grant_role" "grant_root" { + role = "root" + grant_role = "application" + with_admin_option = true +} +``` + +## Argument Reference + +* `role` - (Required) The name of the role that is granted a new membership. +* `grant_role` - (Required) The name of the role that is added to `role`. +* `with_admin_option` - (Optional) Giving ability to grant membership to others or not for `role`. (Default: false) diff --git a/website/postgresql.erb b/website/postgresql.erb index 3c137827..bce73e5e 100644 --- a/website/postgresql.erb +++ b/website/postgresql.erb @@ -25,6 +25,9 @@ > postgresql_grant + > + postgresql_grant_role + > postgresql_role