From c8d57371c5834ea8ea259dfd35f6c4e98c66feaf Mon Sep 17 00:00:00 2001 From: Cyril Gaudin Date: Thu, 22 Nov 2018 14:18:02 +0100 Subject: [PATCH] postgresql_role: Add roles attribute. * This allows to grant roles to the role we are managing. --- postgresql/helpers.go | 11 ++ postgresql/resource_postgresql_role.go | 193 +++++++++++++++----- postgresql/resource_postgresql_role_test.go | 125 ++++++++++--- 3 files changed, 253 insertions(+), 76 deletions(-) diff --git a/postgresql/helpers.go b/postgresql/helpers.go index 2be57bf3..208a0b04 100644 --- a/postgresql/helpers.go +++ b/postgresql/helpers.go @@ -3,6 +3,9 @@ package postgresql import ( "fmt" "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/lib/pq" ) // pqQuoteLiteral returns a string literal safe for inclusion in a PostgreSQL @@ -22,3 +25,11 @@ func validateConnLimit(v interface{}, key string) (warnings []string, errors []e } return } + +func pgArrayToSet(arr pq.ByteaArray) *schema.Set { + s := make([]interface{}, len(arr)) + for i, v := range arr { + s[i] = string(v) + } + return schema.NewSet(schema.HashString, s) +} diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 57762784..eca9a07c 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -27,6 +27,7 @@ const ( roleSkipReassignOwnedAttr = "skip_reassign_owned" roleSuperuserAttr = "superuser" roleValidUntilAttr = "valid_until" + roleRolesAttr = "roles" // Deprecated options roleDepEncryptedAttr = "encrypted" @@ -62,6 +63,14 @@ func resourcePostgreSQLRole() *schema.Resource { Optional: true, Deprecated: fmt.Sprintf("Rename PostgreSQL role resource attribute %q to %q", roleDepEncryptedAttr, roleEncryptedPassAttr), }, + roleRolesAttr: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + MinItems: 0, + Description: "Role(s) to grant to this new role", + }, roleEncryptedPassAttr: { Type: schema.TypeBool, Optional: true, @@ -144,6 +153,12 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro c.catalogLock.Lock() defer c.catalogLock.Unlock() + txn, err := c.DB().Begin() + if err != nil { + return err + } + defer txn.Rollback() + stringOpts := []struct { hclKey string sqlKey string @@ -244,13 +259,21 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro } sql := fmt.Sprintf("CREATE ROLE %s%s", pq.QuoteIdentifier(roleName), createStr) - if _, err := c.DB().Exec(sql); err != nil { - return errwrap.Wrapf(fmt.Sprintf("Error creating role %s: {{err}}", roleName), err) + if _, err := txn.Exec(sql); err != nil { + return errwrap.Wrapf(fmt.Sprintf("error creating role %s: {{err}}", roleName), err) + } + + if err = grantRoles(txn, d); err != nil { + return err + } + + if err = txn.Commit(); err != nil { + return errwrap.Wrapf("could not commit transaction: {{err}}", err) } d.SetId(roleName) - return resourcePostgreSQLRoleReadImpl(d, meta) + return resourcePostgreSQLRoleReadImpl(c, d) } func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) error { @@ -319,16 +342,16 @@ func resourcePostgreSQLRoleRead(d *schema.ResourceData, meta interface{}) error c.catalogLock.RLock() defer c.catalogLock.RUnlock() - return resourcePostgreSQLRoleReadImpl(d, meta) + return resourcePostgreSQLRoleReadImpl(c, d) } -func resourcePostgreSQLRoleReadImpl(d *schema.ResourceData, meta interface{}) error { - c := meta.(*Client) - - roleId := d.Id() +func resourcePostgreSQLRoleReadImpl(c *Client, d *schema.ResourceData) error { var roleSuperuser, roleInherit, roleCreateRole, roleCreateDB, roleCanLogin, roleReplication bool var roleConnLimit int var roleName, roleValidUntil string + var roleRoles pq.ByteaArray + + roleID := d.Id() columns := []string{ "rolname", @@ -342,8 +365,14 @@ func resourcePostgreSQLRoleReadImpl(d *schema.ResourceData, meta interface{}) er `COALESCE(rolvaliduntil::TEXT, 'infinity')`, } - roleSQL := fmt.Sprintf("SELECT %s FROM pg_catalog.pg_roles WHERE rolname=$1", strings.Join(columns, ", ")) - err := c.DB().QueryRow(roleSQL, roleId).Scan( + roleSQL := fmt.Sprintf(`SELECT %s, ARRAY( + SELECT pg_get_userbyid(roleid) FROM pg_catalog.pg_auth_members members WHERE member = pg_roles.oid + ) + FROM pg_catalog.pg_roles WHERE rolname=$1`, + // select columns + strings.Join(columns, ", "), + ) + err := c.DB().QueryRow(roleSQL, roleID).Scan( &roleName, &roleSuperuser, &roleInherit, @@ -353,10 +382,11 @@ func resourcePostgreSQLRoleReadImpl(d *schema.ResourceData, meta interface{}) er &roleReplication, &roleConnLimit, &roleValidUntil, + &roleRoles, ) switch { case err == sql.ErrNoRows: - log.Printf("[WARN] PostgreSQL ROLE (%s) not found", roleId) + log.Printf("[WARN] PostgreSQL ROLE (%s) not found", roleID) d.SetId("") return nil case err != nil: @@ -375,11 +405,12 @@ func resourcePostgreSQLRoleReadImpl(d *schema.ResourceData, meta interface{}) er d.Set(roleSkipReassignOwnedAttr, d.Get(roleSkipReassignOwnedAttr).(bool)) d.Set(roleSuperuserAttr, roleSuperuser) d.Set(roleValidUntilAttr, roleValidUntil) + d.Set(roleRolesAttr, pgArrayToSet(roleRoles)) if c.featureSupported(featureRLS) { var roleBypassRLS bool roleSQL := "SELECT rolbypassrls FROM pg_catalog.pg_roles WHERE rolname=$1" - err = c.DB().QueryRow(roleSQL, roleId).Scan(&roleBypassRLS) + err = c.DB().QueryRow(roleSQL, roleID).Scan(&roleBypassRLS) if err != nil { return errwrap.Wrapf("Error reading RLS properties for ROLE: {{err}}", err) } @@ -394,10 +425,10 @@ func resourcePostgreSQLRoleReadImpl(d *schema.ResourceData, meta interface{}) er } var rolePassword string - err = c.DB().QueryRow("SELECT COALESCE(passwd, '') FROM pg_catalog.pg_shadow AS s WHERE s.usename = $1", roleId).Scan(&rolePassword) + err = c.DB().QueryRow("SELECT COALESCE(passwd, '') FROM pg_catalog.pg_shadow AS s WHERE s.usename = $1", roleID).Scan(&rolePassword) switch { case err == sql.ErrNoRows: - return errwrap.Wrapf(fmt.Sprintf("PostgreSQL role (%s) not found in shadow database: {{err}}", roleId), err) + return errwrap.Wrapf(fmt.Sprintf("PostgreSQL role (%s) not found in shadow database: {{err}}", roleID), err) case err != nil: return errwrap.Wrapf("Error reading role: {{err}}", err) } @@ -411,52 +442,69 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro c.catalogLock.Lock() defer c.catalogLock.Unlock() - db := c.DB() + txn, err := c.DB().Begin() + if err != nil { + return err + } + defer txn.Rollback() + + if err := setRoleName(txn, d); err != nil { + return err + } + + if err := setRoleBypassRLS(c, txn, d); err != nil { + return err + } - if err := setRoleName(db, d); err != nil { + if err := setRoleConnLimit(txn, d); err != nil { return err } - if err := setRoleBypassRLS(c, db, d); err != nil { + if err := setRoleCreateDB(txn, d); err != nil { return err } - if err := setRoleConnLimit(db, d); err != nil { + if err := setRoleCreateRole(txn, d); err != nil { return err } - if err := setRoleCreateDB(db, d); err != nil { + if err := setRoleInherit(txn, d); err != nil { return err } - if err := setRoleCreateRole(db, d); err != nil { + if err := setRoleLogin(txn, d); err != nil { return err } - if err := setRoleInherit(db, d); err != nil { + if err := setRoleReplication(txn, d); err != nil { return err } - if err := setRoleLogin(db, d); err != nil { + if err := setRoleSuperuser(txn, d); err != nil { return err } - if err := setRoleReplication(db, d); err != nil { + if err := setRoleValidUntil(txn, d); err != nil { return err } - if err := setRoleSuperuser(db, d); err != nil { + // applying roles: let's revoke all / grant the right ones + if err = revokeRoles(txn, d); err != nil { return err } - if err := setRoleValidUntil(db, d); err != nil { + if err = grantRoles(txn, d); err != nil { return err } - return resourcePostgreSQLRoleReadImpl(d, meta) + if err = txn.Commit(); err != nil { + return errwrap.Wrapf("could not commit transaction: {{err}}", err) + } + + return resourcePostgreSQLRoleReadImpl(c, d) } -func setRoleName(db *sql.DB, d *schema.ResourceData) error { +func setRoleName(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleNameAttr) { return nil } @@ -469,7 +517,7 @@ func setRoleName(db *sql.DB, d *schema.ResourceData) error { } sql := fmt.Sprintf("ALTER ROLE %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n)) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role NAME: {{err}}", err) } @@ -478,7 +526,7 @@ func setRoleName(db *sql.DB, d *schema.ResourceData) error { return nil } -func setRoleBypassRLS(c *Client, db *sql.DB, d *schema.ResourceData) error { +func setRoleBypassRLS(c *Client, txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleBypassRLSAttr) { return nil } @@ -494,14 +542,14 @@ func setRoleBypassRLS(c *Client, db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role BYPASSRLS: {{err}}", err) } return nil } -func setRoleConnLimit(db *sql.DB, d *schema.ResourceData) error { +func setRoleConnLimit(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleConnLimitAttr) { return nil } @@ -509,14 +557,14 @@ func setRoleConnLimit(db *sql.DB, d *schema.ResourceData) error { connLimit := d.Get(roleConnLimitAttr).(int) roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s CONNECTION LIMIT %d", pq.QuoteIdentifier(roleName), connLimit) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role CONNECTION LIMIT: {{err}}", err) } return nil } -func setRoleCreateDB(db *sql.DB, d *schema.ResourceData) error { +func setRoleCreateDB(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleCreateDBAttr) { return nil } @@ -528,14 +576,14 @@ func setRoleCreateDB(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role CREATEDB: {{err}}", err) } return nil } -func setRoleCreateRole(db *sql.DB, d *schema.ResourceData) error { +func setRoleCreateRole(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleCreateRoleAttr) { return nil } @@ -547,14 +595,14 @@ func setRoleCreateRole(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role CREATEROLE: {{err}}", err) } return nil } -func setRoleInherit(db *sql.DB, d *schema.ResourceData) error { +func setRoleInherit(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleInheritAttr) { return nil } @@ -566,14 +614,14 @@ func setRoleInherit(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role INHERIT: {{err}}", err) } return nil } -func setRoleLogin(db *sql.DB, d *schema.ResourceData) error { +func setRoleLogin(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleLoginAttr) { return nil } @@ -585,14 +633,14 @@ func setRoleLogin(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role LOGIN: {{err}}", err) } return nil } -func setRoleReplication(db *sql.DB, d *schema.ResourceData) error { +func setRoleReplication(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleReplicationAttr) { return nil } @@ -604,14 +652,14 @@ func setRoleReplication(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role REPLICATION: {{err}}", err) } return nil } -func setRoleSuperuser(db *sql.DB, d *schema.ResourceData) error { +func setRoleSuperuser(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleSuperuserAttr) { return nil } @@ -623,14 +671,14 @@ func setRoleSuperuser(db *sql.DB, d *schema.ResourceData) error { } roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role SUPERUSER: {{err}}", err) } return nil } -func setRoleValidUntil(db *sql.DB, d *schema.ResourceData) error { +func setRoleValidUntil(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleValidUntilAttr) { return nil } @@ -644,9 +692,62 @@ func setRoleValidUntil(db *sql.DB, d *schema.ResourceData) error { roleName := d.Get(roleNameAttr).(string) sql := fmt.Sprintf("ALTER ROLE %s VALID UNTIL '%s'", pq.QuoteIdentifier(roleName), pqQuoteLiteral(validUntil)) - if _, err := db.Exec(sql); err != nil { + if _, err := txn.Exec(sql); err != nil { return errwrap.Wrapf("Error updating role VALID UNTIL: {{err}}", err) } return nil } + +func revokeRoles(txn *sql.Tx, d *schema.ResourceData) error { + role := d.Get(roleNameAttr).(string) + + query := `SELECT pg_get_userbyid(roleid) + FROM pg_catalog.pg_auth_members members + JOIN pg_catalog.pg_roles ON members.member = pg_roles.oid + WHERE rolname = $1` + + rows, err := txn.Query(query, role) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("could not get roles list for role %s: {{err}}", role), err) + } + defer rows.Close() + + grantedRoles := []string{} + for rows.Next() { + var grantedRole string + + if err = rows.Scan(&grantedRole); err != nil { + return errwrap.Wrapf(fmt.Sprintf("could not scan role name for role %s: {{err}}", role), err) + } + // We cannot revoke directly here as it shares the same cursor (with Tx) + // and rows.Next seems to retrieve result row by row. + // see: https://github.com/lib/pq/issues/81 + grantedRoles = append(grantedRoles, grantedRole) + } + + for _, grantedRole := range grantedRoles { + query = fmt.Sprintf("REVOKE %s FROM %s", pq.QuoteIdentifier(grantedRole), pq.QuoteIdentifier(role)) + + log.Printf("[DEBUG] revoking role %s from %s", grantedRole, role) + if _, err := txn.Exec(query); err != nil { + return errwrap.Wrapf(fmt.Sprintf("could not revoke role %s from %s: {{err}}", string(grantedRole), role), err) + } + } + + return nil +} + +func grantRoles(txn *sql.Tx, d *schema.ResourceData) error { + role := d.Get(roleNameAttr).(string) + + for _, grantingRole := range d.Get("roles").(*schema.Set).List() { + query := fmt.Sprintf( + "GRANT %s TO %s", pq.QuoteIdentifier(grantingRole.(string)), pq.QuoteIdentifier(role), + ) + if _, err := txn.Exec(query); err != nil { + return errwrap.Wrapf(fmt.Sprintf("could not grant role %s to %s: {{err}}", grantingRole, role), err) + } + } + return nil +} diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index cee3b00c..49969b5d 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -3,6 +3,8 @@ package postgresql import ( "database/sql" "fmt" + "reflect" + "sort" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -18,8 +20,13 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { { Config: testAccPostgresqlRoleConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("postgresql_role.myrole2", "true"), - resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "name", "testing_role_with_defaults"), + testAccCheckPostgresqlRoleExists("tf_tests_myrole2", nil), + resource.TestCheckResourceAttr("postgresql_role.myrole2", "name", "tf_tests_myrole2"), + resource.TestCheckResourceAttr("postgresql_role.myrole2", "login", "true"), + resource.TestCheckResourceAttr("postgresql_role.myrole2", "roles.#", "0"), + + testAccCheckPostgresqlRoleExists("tf_tests_role_default", nil), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "name", "tf_tests_role_default"), 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"), @@ -32,6 +39,15 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "valid_until", "infinity"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "skip_drop_role", "false"), resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "skip_reassign_owned", "false"), + resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "roles.#", "0"), + + testAccCheckPostgresqlRoleExists("tf_tests_sub_role", []string{"tf_tests_myrole2", "tf_tests_role_simple"}), + resource.TestCheckResourceAttr("postgresql_role.sub_role", "name", "tf_tests_sub_role"), + resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.#", "2"), + + // The int part in the attr name is the schema.HashString of the value. + resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.1456111905", "tf_tests_myrole2"), + resource.TestCheckResourceAttr("postgresql_role.sub_role", "roles.3803627293", "tf_tests_role_simple"), ), }, }, @@ -47,19 +63,38 @@ func TestAccPostgresqlRole_Update(t *testing.T) { { Config: testAccPostgresqlRoleUpdate1Config, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("postgresql_role.update_role", "true"), - resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "update_role"), + testAccCheckPostgresqlRoleExists("tf_tests_update_role", []string{}), + resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "tf_tests_update_role"), resource.TestCheckResourceAttr("postgresql_role.update_role", "login", "true"), resource.TestCheckResourceAttr("postgresql_role.update_role", "connection_limit", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "roles.#", "0"), ), }, { Config: testAccPostgresqlRoleUpdate2Config, Check: resource.ComposeTestCheckFunc( - testAccCheckPostgresqlRoleExists("postgresql_role.update_role", "true"), - resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "update_role2"), + testAccCheckPostgresqlRoleExists("tf_tests_update_role2", []string{"tf_tests_group_role"}), + resource.TestCheckResourceAttr( + "postgresql_role.update_role", "name", "tf_tests_update_role2", + ), resource.TestCheckResourceAttr("postgresql_role.update_role", "login", "true"), resource.TestCheckResourceAttr("postgresql_role.update_role", "connection_limit", "5"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "roles.#", "1"), + // The int part in the attr name is the schema.HashString of the value. + resource.TestCheckResourceAttr( + "postgresql_role.update_role", "roles.2634717634", "tf_tests_group_role", + ), + ), + }, + // apply again the first one to tests the granted role is correctly revoked + { + Config: testAccPostgresqlRoleUpdate1Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlRoleExists("tf_tests_update_role", []string{}), + resource.TestCheckResourceAttr("postgresql_role.update_role", "name", "tf_tests_update_role"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "login", "true"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "connection_limit", "-1"), + resource.TestCheckResourceAttr("postgresql_role.update_role", "roles.#", "0"), ), }, }, @@ -88,25 +123,11 @@ func testAccCheckPostgresqlRoleDestroy(s *terraform.State) error { return nil } -func testAccCheckPostgresqlRoleExists(n string, canLogin string) resource.TestCheckFunc { +func testAccCheckPostgresqlRoleExists(roleName string, grantedRoles []string) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Resource not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - actualCanLogin := rs.Primary.Attributes["login"] - if actualCanLogin != canLogin { - return fmt.Errorf("Wrong value for login expected %s got %s", canLogin, actualCanLogin) - } - client := testAccProvider.Meta().(*Client) - exists, err := checkRoleExists(client, rs.Primary.ID) + exists, err := checkRoleExists(client, roleName) if err != nil { return fmt.Errorf("Error checking role %s", err) } @@ -115,6 +136,9 @@ func testAccCheckPostgresqlRoleExists(n string, canLogin string) resource.TestCh return fmt.Errorf("Role not found") } + if grantedRoles != nil { + return checkGrantedRoles(client, roleName, grantedRoles) + } return nil } } @@ -132,36 +156,65 @@ func checkRoleExists(client *Client, roleName string) (bool, error) { return true, nil } +func checkGrantedRoles(client *Client, roleName string, expectedRoles []string) error { + rows, err := client.DB().Query( + "SELECT role_name FROM information_schema.applicable_roles WHERE grantee=$1 ORDER BY role_name", + roleName, + ) + if err != nil { + return fmt.Errorf("Error reading granted roles: %v", err) + } + defer rows.Close() + + grantedRoles := []string{} + for rows.Next() { + var grantedRole string + if err := rows.Scan(&grantedRole); err != nil { + return fmt.Errorf("Error scanning granted role: %v", err) + } + grantedRoles = append(grantedRoles, grantedRole) + } + + sort.Strings(expectedRoles) + if !reflect.DeepEqual(grantedRoles, expectedRoles) { + return fmt.Errorf( + "Role %s is not a members of the expected list of roles. expected %v - got %v", + roleName, expectedRoles, grantedRoles, + ) + } + return nil +} + var testAccPostgresqlRoleConfig = ` resource "postgresql_role" "myrole2" { - name = "myrole2" + name = "tf_tests_myrole2" login = true } resource "postgresql_role" "role_with_pwd" { - name = "role_with_pwd" + name = "tf_tests_role_with_pwd" login = true password = "mypass" } resource "postgresql_role" "role_with_pwd_encr" { - name = "role_with_pwd_encr" + name = "tf_tests_role_with_pwd_encr" login = true password = "mypass" encrypted = true } resource "postgresql_role" "role_with_pwd_no_login" { - name = "role_with_pwd_no_login" + name = "tf_tests_role_with_pwd_no_login" password = "mypass" } resource "postgresql_role" "role_simple" { - name = "role_simple" + name = "tf_tests_role_simple" } resource "postgresql_role" "role_with_defaults" { - name = "testing_role_with_defaults" + name = "tf_tests_role_default" superuser = false create_database = false create_role = false @@ -176,19 +229,31 @@ resource "postgresql_role" "role_with_defaults" { skip_reassign_owned = false valid_until = "infinity" } + +resource "postgresql_role" "sub_role" { + name = "tf_tests_sub_role" + roles = [ + "${postgresql_role.myrole2.id}", + "${postgresql_role.role_simple.id}", + ] +} ` var testAccPostgresqlRoleUpdate1Config = ` resource "postgresql_role" "update_role" { - name = "update_role" + name = "tf_tests_update_role" login = true } ` var testAccPostgresqlRoleUpdate2Config = ` +resource "postgresql_role" "group_role" { + name = "tf_tests_group_role" +} resource "postgresql_role" "update_role" { - name = "update_role2" + name = "tf_tests_update_role2" login = true connection_limit = 5 + roles = ["${postgresql_role.group_role.name}"] } `