Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions postgresql/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,35 @@ func withRolesGranted(txn *sql.Tx, roles []string, fn func() error) error {
return nil
}

// listDatabases returns the list of all databases accessible for REASSIGN OWNED operations.
// It excludes template databases and databases that don't allow connections.
func listDatabases(db QueryAble) ([]string, error) {
rows, err := db.Query("SELECT datname FROM pg_database WHERE datallowconn = true AND NOT datistemplate AND has_database_privilege(current_user, datname, 'CONNECT')")
if err != nil {
return nil, fmt.Errorf("could not list databases: %w", err)
}
defer func() {
if closeErr := rows.Close(); closeErr != nil {
log.Printf("[WARN] could not close rows: %v", closeErr)
}
}()

var databases []string
for rows.Next() {
var dbName string
if err := rows.Scan(&dbName); err != nil {
return nil, fmt.Errorf("could not scan database name: %w", err)
}
databases = append(databases, dbName)
}

if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating database rows: %w", err)
}

return databases, nil
}

func sliceContainsStr(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
Expand Down
58 changes: 48 additions & 10 deletions postgresql/resource_postgresql_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
roleReplicationAttr = "replication"
roleSkipDropRoleAttr = "skip_drop_role"
roleSkipReassignOwnedAttr = "skip_reassign_owned"
roleReassignOwnedToAttr = "reassign_owned_to"
roleSuperuserAttr = "superuser"
roleValidUntilAttr = "valid_until"
roleRolesAttr = "roles"
Expand Down Expand Up @@ -182,6 +183,11 @@ func resourcePostgreSQLRole() *schema.Resource {
Default: false,
Description: "Skip actually running the REASSIGN OWNED command when removing a role from PostgreSQL",
},
roleReassignOwnedToAttr: {
Type: schema.TypeString,
Optional: true,
Description: "Role to reassign owned objects to when removing a role from PostgreSQL. If not specified, defaults to the current user",
},
roleStatementTimeoutAttr: {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -393,28 +399,59 @@ func resourcePostgreSQLRoleDelete(db *DBConnection, d *schema.ResourceData) erro
}

if !d.Get(roleSkipReassignOwnedAttr).(bool) {
if err := withRolesGranted(txn, []string{roleName}, func() error {
currentUser := db.client.config.getDatabaseUsername()
if _, err := txn.Exec(fmt.Sprintf("REASSIGN OWNED BY %s TO %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(currentUser))); err != nil {
return fmt.Errorf("could not reassign owned by role %s to %s: %w", roleName, currentUser, err)
// Use the specified reassign_owned_to user, or fall back to current user
reassignTo := d.Get(roleReassignOwnedToAttr).(string)
if reassignTo == "" {
reassignTo = db.client.config.getDatabaseUsername()
}

// Get list of all databases to run REASSIGN OWNED in each one
databases, err := listDatabases(txn)
if err != nil {
return fmt.Errorf("error committing initial transaction: %w", err)
}

// Execute REASSIGN OWNED and DROP OWNED in each database
for _, dbName := range databases {
log.Printf("[DEBUG] Running REASSIGN OWNED in database %s", dbName)

dbTxn, err := startTransaction(db.client, dbName)
if err != nil {
return fmt.Errorf("could not start transaction in database %s: %w", dbName, err)
}

if _, err := txn.Exec(fmt.Sprintf("DROP OWNED BY %s", pq.QuoteIdentifier(roleName))); err != nil {
return fmt.Errorf("could not drop owned by role %s: %w", roleName, err)
if err := withRolesGranted(dbTxn, []string{roleName, reassignTo}, func() error {
log.Printf("reassignOwned: reassign owned by role %s to %s in database %s", roleName, reassignTo, dbName)

if _, err := dbTxn.Exec(fmt.Sprintf("REASSIGN OWNED BY %s TO %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(reassignTo))); err != nil {
return fmt.Errorf("could not reassign owned by role %s to %s in database %s: %w", roleName, reassignTo, dbName, err)
}

if _, err := dbTxn.Exec(fmt.Sprintf("DROP OWNED BY %s", pq.QuoteIdentifier(roleName))); err != nil {
return fmt.Errorf("could not drop owned by role %s in database %s: %w", roleName, dbName, err)
}
return nil
}); err != nil {
if rollbackErr := dbTxn.Rollback(); rollbackErr != nil {
log.Printf("[WARN] could not rollback transaction in database %s: %v", dbName, rollbackErr)
}
return err
}

if err := dbTxn.Commit(); err != nil {
return fmt.Errorf("error committing transaction in database %s: %w", dbName, err)
}
return nil
}); err != nil {
return err
}
}

if !d.Get(roleSkipDropRoleAttr).(bool) {
if _, err := txn.Exec(fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))); err != nil {
return fmt.Errorf("could not delete role %s: %w", roleName, err)
}
}

if err := txn.Commit(); err != nil {
return fmt.Errorf("error committing schema: %w", err)
return fmt.Errorf("error committing final transaction: %w", err)
}

d.SetId("")
Expand Down Expand Up @@ -509,6 +546,7 @@ func resourcePostgreSQLRoleReadImpl(db *DBConnection, d *schema.ResourceData) er
d.Set(roleLoginAttr, roleCanLogin)
d.Set(roleSkipDropRoleAttr, d.Get(roleSkipDropRoleAttr).(bool))
d.Set(roleSkipReassignOwnedAttr, d.Get(roleSkipReassignOwnedAttr).(bool))
d.Set(roleReassignOwnedToAttr, d.Get(roleReassignOwnedToAttr).(string))
d.Set(roleSuperuserAttr, roleSuperuser)
d.Set(roleValidUntilAttr, roleValidUntil)
d.Set(roleReplicationAttr, roleReplication)
Expand Down
38 changes: 38 additions & 0 deletions postgresql/resource_postgresql_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ 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", "reassign_owned_to", ""),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "statement_timeout", "0"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "idle_in_transaction_session_timeout", "0"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "assume_role", ""),
Expand Down Expand Up @@ -238,6 +239,43 @@ resource "postgresql_role" "test_role" {
})
}

// Test reassign_owned_to parameter
func TestAccPostgresqlRole_ReassignOwnedTo(t *testing.T) {
// Get the admin user (usually postgres) who will receive the reassigned objects
admin := os.Getenv("PGUSER")
if admin == "" {
admin = "postgres"
}

config := fmt.Sprintf(`
resource "postgresql_role" "temp_role" {
name = "temp_role"
login = true
password = "temppass"
reassign_owned_to = "%s"
}
`, admin)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testCheckCompatibleVersion(t, featurePrivileges)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlRoleDestroy,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlRoleExists("temp_role", nil, nil),
resource.TestCheckResourceAttr("postgresql_role.temp_role", "name", "temp_role"),
resource.TestCheckResourceAttr("postgresql_role.temp_role", "reassign_owned_to", admin),
),
},
},
})
}

func testAccCheckPostgresqlRoleDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*Client)

Expand Down
12 changes: 12 additions & 0 deletions website/docs/r/postgresql_role.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ resource "postgresql_role" "secure_role" {
password_wo = "secure_password_123"
password_wo_version = "1"
}

# Example with reassign_owned_to
resource "postgresql_role" "temp_role" {
name = "temp_role"
login = true
password = "temppass"
reassign_owned_to = "admin_role" # Objects will be reassigned to admin_role when this role is dropped
}
```

## Write-Only Password Management
Expand Down Expand Up @@ -166,6 +174,10 @@ resource "postgresql_role" "app_user" {
an implicit
[`DROP OWNED`](https://www.postgresql.org/docs/current/static/sql-drop-owned.html)).

* `reassign_owned_to` - (Optional) Specifies the role to which objects owned by the role being
dropped should be reassigned. If not specified, defaults to the current database user
(the user configured in the provider). This is only used when `skip_reassign_owned` is `false`.

* `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).
Expand Down