Skip to content

Commit 59a36ae

Browse files
lukaalbaLukas Albanicyrilgdn
authored
postgresql_database: Reassign objects owners if database owner changes (#458)
Hi, this is my first Pull Request, so if something is missing or wrong, let me know. To resolve [439](#439) I suggest to add an additional parameter to the database resource called `alter_object_ownership`. If set to true, it will issue a REASSIGN OWNED BY query on the database when the owner changes, transfer the whole ownership in the database from the previous owner to the new one. As far as I can tell this is only possible if the user in the provider configuration is a superuser or a user which has grants for both the previous owner role and the new one. So I skipped the test in the rds-like test suite. --------- Co-authored-by: Lukas Albani <lukas.albani@deutschebahn.com> Co-authored-by: Cyril Gaudin <cyril.gaudin@gmail.com>
1 parent c3f634b commit 59a36ae

File tree

3 files changed

+201
-16
lines changed

3 files changed

+201
-16
lines changed

postgresql/resource_postgresql_database.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import (
1414
)
1515

1616
const (
17-
dbAllowConnsAttr = "allow_connections"
18-
dbCTypeAttr = "lc_ctype"
19-
dbCollationAttr = "lc_collate"
20-
dbConnLimitAttr = "connection_limit"
21-
dbEncodingAttr = "encoding"
22-
dbIsTemplateAttr = "is_template"
23-
dbNameAttr = "name"
24-
dbOwnerAttr = "owner"
25-
dbTablespaceAttr = "tablespace_name"
26-
dbTemplateAttr = "template"
17+
dbAllowConnsAttr = "allow_connections"
18+
dbCTypeAttr = "lc_ctype"
19+
dbCollationAttr = "lc_collate"
20+
dbConnLimitAttr = "connection_limit"
21+
dbEncodingAttr = "encoding"
22+
dbIsTemplateAttr = "is_template"
23+
dbNameAttr = "name"
24+
dbOwnerAttr = "owner"
25+
dbTablespaceAttr = "tablespace_name"
26+
dbTemplateAttr = "template"
27+
dbAlterObjectOwnership = "alter_object_ownership"
2728
)
2829

2930
func resourcePostgreSQLDatabase() *schema.Resource {
@@ -102,6 +103,12 @@ func resourcePostgreSQLDatabase() *schema.Resource {
102103
Computed: true,
103104
Description: "If true, then this database can be cloned by any user with CREATEDB privileges",
104105
},
106+
dbAlterObjectOwnership: {
107+
Type: schema.TypeBool,
108+
Optional: true,
109+
Default: false,
110+
Description: "If true, the owner of already existing objects will change if the owner changes",
111+
},
105112
},
106113
}
107114
}
@@ -393,6 +400,10 @@ func resourcePostgreSQLDatabaseUpdate(db *DBConnection, d *schema.ResourceData)
393400
return err
394401
}
395402

403+
if err := setAlterOwnership(db, d); err != nil {
404+
return err
405+
}
406+
396407
if err := setDBOwner(db, d); err != nil {
397408
return err
398409
}
@@ -468,6 +479,7 @@ func setDBOwner(db *DBConnection, d *schema.ResourceData) error {
468479
}
469480

470481
dbName := d.Get(dbNameAttr).(string)
482+
471483
sql := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
472484
if _, err := db.Exec(sql); err != nil {
473485
return fmt.Errorf("Error updating database OWNER: %w", err)
@@ -476,6 +488,60 @@ func setDBOwner(db *DBConnection, d *schema.ResourceData) error {
476488
return err
477489
}
478490

491+
func setAlterOwnership(db *DBConnection, d *schema.ResourceData) error {
492+
if !d.HasChange(dbOwnerAttr) && !d.HasChange(dbAlterObjectOwnership) {
493+
return nil
494+
}
495+
owner := d.Get(dbOwnerAttr).(string)
496+
if owner == "" {
497+
return nil
498+
}
499+
500+
alterOwnership := d.Get(dbAlterObjectOwnership).(bool)
501+
if !alterOwnership {
502+
return nil
503+
}
504+
currentUser := db.client.config.getDatabaseUsername()
505+
506+
dbName := d.Get(dbNameAttr).(string)
507+
508+
lockTxn, err := startTransaction(db.client, dbName)
509+
if err := pgLockRole(lockTxn, currentUser); err != nil {
510+
return err
511+
}
512+
defer deferredRollback(lockTxn)
513+
514+
currentOwner, err := getDatabaseOwner(db, dbName)
515+
if err != nil {
516+
return fmt.Errorf("Error getting current database OWNER: %w", err)
517+
}
518+
519+
newOwner := d.Get(dbOwnerAttr).(string)
520+
521+
if currentOwner == newOwner {
522+
return nil
523+
}
524+
525+
currentOwnerGranted, err := grantRoleMembership(db, currentOwner, currentUser)
526+
if err != nil {
527+
return err
528+
}
529+
if currentOwnerGranted {
530+
defer func() {
531+
_, err = revokeRoleMembership(db, currentOwner, currentUser)
532+
}()
533+
}
534+
sql := fmt.Sprintf("REASSIGN OWNED BY %s TO %s", pq.QuoteIdentifier(currentOwner), pq.QuoteIdentifier(newOwner))
535+
if _, err := lockTxn.Exec(sql); err != nil {
536+
return fmt.Errorf("Error reassigning objects owned by '%s': %w", currentOwner, err)
537+
}
538+
539+
if err := lockTxn.Commit(); err != nil {
540+
return fmt.Errorf("error committing reassign: %w", err)
541+
}
542+
return nil
543+
}
544+
479545
func setDBTablespace(db QueryAble, d *schema.ResourceData) error {
480546
if !d.HasChange(dbTablespaceAttr) {
481547
return nil

postgresql/resource_postgresql_database_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
4343
"postgresql_database.default_opts", "connection_limit", "-1"),
4444
resource.TestCheckResourceAttr(
4545
"postgresql_database.default_opts", "is_template", "false"),
46+
resource.TestCheckResourceAttr(
47+
"postgresql_database.default_opts", "alter_object_ownership", "false"),
4648

4749
resource.TestCheckResourceAttr(
4850
"postgresql_database.modified_opts", "owner", "myrole"),
@@ -62,6 +64,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
6264
"postgresql_database.modified_opts", "connection_limit", "10"),
6365
resource.TestCheckResourceAttr(
6466
"postgresql_database.modified_opts", "is_template", "true"),
67+
resource.TestCheckResourceAttr(
68+
"postgresql_database.modified_opts", "alter_object_ownership", "true"),
6569

6670
resource.TestCheckResourceAttr(
6771
"postgresql_database.pathological_opts", "owner", "myrole"),
@@ -266,6 +270,78 @@ resource postgresql_database "test_db" {
266270
})
267271
}
268272

273+
// Test the case where the owned objects by the previous database owner are altered.
274+
func TestAccPostgresqlDatabase_AlterObjectOwnership(t *testing.T) {
275+
skipIfNotAcc(t)
276+
277+
const (
278+
databaseSuffix = "ownership"
279+
tableName = "testtable1"
280+
previous_owner = "previous_owner"
281+
new_owner = "new_owner"
282+
)
283+
284+
databaseName := fmt.Sprintf("%s_%s", dbNamePrefix, databaseSuffix)
285+
286+
config := getTestConfig(t)
287+
dsn := config.connStr("postgres")
288+
289+
for _, role := range []string{previous_owner, new_owner} {
290+
dbExecute(
291+
t, dsn,
292+
fmt.Sprintf("CREATE ROLE %s;", role),
293+
)
294+
defer func(role string) {
295+
dbExecute(t, dsn, fmt.Sprintf("DROP ROLE %s", role))
296+
}(role)
297+
298+
}
299+
300+
resource.Test(t, resource.TestCase{
301+
PreCheck: func() {
302+
testAccPreCheck(t)
303+
testSuperuserPreCheck(t)
304+
},
305+
Providers: testAccProviders,
306+
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
307+
Steps: []resource.TestStep{
308+
{
309+
Config: `
310+
resource postgresql_database "test_db" {
311+
name = "tf_tests_db_ownership"
312+
owner = "previous_owner"
313+
alter_object_ownership = true
314+
}
315+
`,
316+
Check: func(*terraform.State) error {
317+
// To test default privileges, we need to create a table
318+
// after having apply the state.
319+
_ = createTestTables(t, databaseSuffix, []string{tableName}, previous_owner)
320+
return nil
321+
},
322+
},
323+
{
324+
Config: `
325+
resource postgresql_database "test_db" {
326+
name = "tf_tests_db_ownership"
327+
owner = "new_owner"
328+
alter_object_ownership = true
329+
}
330+
`,
331+
Check: resource.ComposeTestCheckFunc(
332+
testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"),
333+
resource.TestCheckResourceAttr("postgresql_database.test_db", "name", databaseName),
334+
resource.TestCheckResourceAttr("postgresql_database.test_db", "owner", new_owner),
335+
resource.TestCheckResourceAttr("postgresql_database.test_db", "alter_object_ownership", "true"),
336+
337+
checkTableOwnership(t, config.connStr(databaseName), new_owner, tableName),
338+
),
339+
},
340+
},
341+
})
342+
343+
}
344+
269345
func checkUserMembership(
270346
t *testing.T, dsn, member, role string, shouldHaveRole bool,
271347
) resource.TestCheckFunc {
@@ -306,6 +382,38 @@ func checkUserMembership(
306382
}
307383
}
308384

385+
func checkTableOwnership(
386+
t *testing.T, dsn, owner, tableName string,
387+
) resource.TestCheckFunc {
388+
return func(s *terraform.State) error {
389+
db, err := sql.Open("postgres", dsn)
390+
if err != nil {
391+
t.Fatalf("could not create connection pool: %v", err)
392+
}
393+
defer db.Close()
394+
395+
var _rez int
396+
397+
err = db.QueryRow(`
398+
SELECT 1 FROM pg_tables
399+
WHERE tablename = $1 AND tableowner = $2
400+
`, tableName, owner).Scan(&_rez)
401+
402+
switch {
403+
case err == sql.ErrNoRows:
404+
return fmt.Errorf(
405+
"User %s should be owner of %s but is not", owner, tableName,
406+
)
407+
case err != nil:
408+
t.Fatalf("Error checking table ownership. %v", err)
409+
410+
}
411+
412+
return nil
413+
414+
}
415+
}
416+
309417
func testAccCheckPostgresqlDatabaseDestroy(s *terraform.State) error {
310418
client := testAccProvider.Meta().(*Client)
311419

@@ -396,6 +504,7 @@ resource "postgresql_database" "default_opts" {
396504
lc_ctype = "C"
397505
connection_limit = -1
398506
is_template = false
507+
alter_object_ownership = false
399508
}
400509
401510
resource "postgresql_database" "modified_opts" {
@@ -407,6 +516,7 @@ resource "postgresql_database" "modified_opts" {
407516
lc_ctype = "en_US.UTF-8"
408517
connection_limit = 10
409518
is_template = true
519+
alter_object_ownership = true
410520
}
411521
412522
resource "postgresql_database" "pathological_opts" {

website/docs/r/postgresql_database.html.markdown

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ within a PostgreSQL server instance.
1717

1818
```hcl
1919
resource "postgresql_database" "my_db" {
20-
name = "my_db"
21-
owner = "my_role"
22-
template = "template0"
23-
lc_collate = "C"
24-
connection_limit = -1
25-
allow_connections = true
20+
name = "my_db"
21+
owner = "my_role"
22+
template = "template0"
23+
lc_collate = "C"
24+
connection_limit = -1
25+
allow_connections = true
26+
alter_object_ownership = true
2627
}
2728
```
2829

@@ -82,6 +83,14 @@ resource "postgresql_database" "my_db" {
8283
force the creation of a new resource as this value can only be changed when a
8384
database is created.
8485

86+
* `alter_object_ownership` - (Optional) If `true`, the change of the database
87+
`owner` will also include a reassignment of the ownership of preexisting
88+
objects like tables or sequences from the previous owner to the new one.
89+
If set to `false` (the default), then the previous database `owner` will still
90+
hold the ownership of the objects in that database. To alter existing objects in
91+
the database, you must be a direct or indirect member of the specified role, or
92+
the username in the provider must be superuser.
93+
8594
## Import Example
8695

8796
`postgresql_database` supports importing resources. Supposing the following

0 commit comments

Comments
 (0)