Skip to content

Commit 6d26b59

Browse files
authored
Fix #321 replaces postgresql_grant all the time. (#476)
Fixes #321 Fix based on [doctolib's fork](doctolib@1caee37) version 1.19.0, with [PR 135](#135), the postgresql_grant resource gets re-created when there is a change. Replacing the resource is not a good idea because the "destroy/create" operations are completely separate. i.e. they are not atomic which means (given the example in the "Steps to Reproduce" section above) for a short moment between the 2 operations the public role loses access to the public schema. If for any reason Terraform fails midway or it gets interrupted, users will end up not being able to access the objects in the public schema. This is what happens in the PostgreSQL log: ``` 2023-07-11 14:50:05.989 UTC [1673] LOG: statement: BEGIN READ WRITE 2023-07-11 14:50:06.000 UTC [1673] LOG: statement: REVOKE ALL PRIVILEGES ON SCHEMA "public" FROM "public" 2023-07-11 14:50:06.001 UTC [1673] LOG: statement: COMMIT 2023-07-11 14:50:06.033 UTC [1675] LOG: statement: BEGIN READ WRITE 2023-07-11 14:50:06.043 UTC [1675] LOG: statement: REVOKE ALL PRIVILEGES ON SCHEMA "public" FROM "public" 2023-07-11 14:50:06.044 UTC [1675] LOG: statement: GRANT USAGE ON SCHEMA "public" TO "public" 2023-07-11 14:50:06.045 UTC [1675] LOG: statement: COMMIT ``` In our case we're only removing ForceNew from privileges, as this fixes our use case, but the overall solution allows every schema to be updated instead of recreated. Introduced a "getter" in order to fix [PR 135](#135) original issue that caused the introduction of "ForceNew". > Originally, the privileges argument did not force recreation of the resource. This was a problem because it meant that when changing the privileges in a grant resource, the update function would be triggered and would receive only the new configuration. So the revocation would not revoke the old permissions, but the new one, which is not very useful. .... I could not find a way to fetch the privilege stored in the state, & setting the argument to ForceNew solved this problem. So I did that. Fetching the privilege stored in state is the job of our new getter, this way we don't have to "ForceNew" everything. I think we might be able to keep a single "Create" function if we wanted, checking d.IsResourceNew() to decide if we should use the old one or new one, but the solution from doctolib seems robust enough.
1 parent 7120473 commit 6d26b59

File tree

2 files changed

+55
-35
lines changed

2 files changed

+55
-35
lines changed

postgresql/resource_postgresql_grant.go

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ var objectTypes = map[string]string{
3434
"schema": "n",
3535
}
3636

37+
type ResourceSchemeGetter func(string) interface{}
38+
3739
func resourcePostgreSQLGrant() *schema.Resource {
3840
return &schema.Resource{
3941
Create: PGResourceFunc(resourcePostgreSQLGrantCreate),
40-
// Since all of this resource's arguments force a recreation
41-
// there's no need for an Update function
42-
// Update:
42+
Update: PGResourceFunc(resourcePostgreSQLGrantUpdate),
4343
Read: PGResourceFunc(resourcePostgreSQLGrantRead),
4444
Delete: PGResourceFunc(resourcePostgreSQLGrantDelete),
4545

@@ -88,7 +88,6 @@ func resourcePostgreSQLGrant() *schema.Resource {
8888
"privileges": {
8989
Type: schema.TypeSet,
9090
Required: true,
91-
ForceNew: true,
9291
Elem: &schema.Schema{Type: schema.TypeString},
9392
Set: schema.HashString,
9493
Description: "The list of privileges to grant",
@@ -129,6 +128,14 @@ func resourcePostgreSQLGrantRead(db *DBConnection, d *schema.ResourceData) error
129128
}
130129

131130
func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) error {
131+
return resourcePostgreSQLGrantCreateOrUpdate(db, d, false)
132+
}
133+
134+
func resourcePostgreSQLGrantUpdate(db *DBConnection, d *schema.ResourceData) error {
135+
return resourcePostgreSQLGrantCreateOrUpdate(db, d, true)
136+
}
137+
138+
func resourcePostgreSQLGrantCreateOrUpdate(db *DBConnection, d *schema.ResourceData, usePrevious bool) error {
132139
if err := validateFeatureSupport(db, d); err != nil {
133140
return fmt.Errorf("feature is not supported: %v", err)
134141
}
@@ -187,7 +194,7 @@ func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) err
187194
// Revoke all privileges before granting otherwise reducing privileges will not work.
188195
// We just have to revoke them in the same transaction so the role will not lost its
189196
// privileges between the revoke and grant statements.
190-
if err := revokeRolePrivileges(txn, d); err != nil {
197+
if err := revokeRolePrivileges(txn, d, usePrevious); err != nil {
191198
return err
192199
}
193200
if err := grantRolePrivileges(txn, d); err != nil {
@@ -243,7 +250,7 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err
243250
}
244251

245252
if err := withRolesGranted(txn, owners, func() error {
246-
return revokeRolePrivileges(txn, d)
253+
return revokeRolePrivileges(txn, d, false)
247254
}); err != nil {
248255
return err
249256
}
@@ -589,40 +596,40 @@ func createGrantQuery(d *schema.ResourceData, privileges []string) string {
589596
return query
590597
}
591598

592-
func createRevokeQuery(d *schema.ResourceData) string {
599+
func createRevokeQuery(getter ResourceSchemeGetter) string {
593600
var query string
594601

595-
switch strings.ToUpper(d.Get("object_type").(string)) {
602+
switch strings.ToUpper(getter("object_type").(string)) {
596603
case "DATABASE":
597604
query = fmt.Sprintf(
598605
"REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s",
599-
pq.QuoteIdentifier(d.Get("database").(string)),
600-
pq.QuoteIdentifier(d.Get("role").(string)),
606+
pq.QuoteIdentifier(getter("database").(string)),
607+
pq.QuoteIdentifier(getter("role").(string)),
601608
)
602609
case "SCHEMA":
603610
query = fmt.Sprintf(
604611
"REVOKE ALL PRIVILEGES ON SCHEMA %s FROM %s",
605-
pq.QuoteIdentifier(d.Get("schema").(string)),
606-
pq.QuoteIdentifier(d.Get("role").(string)),
612+
pq.QuoteIdentifier(getter("schema").(string)),
613+
pq.QuoteIdentifier(getter("role").(string)),
607614
)
608615
case "FOREIGN_DATA_WRAPPER":
609-
fdwName := d.Get("objects").(*schema.Set).List()[0]
616+
fdwName := getter("objects").(*schema.Set).List()[0]
610617
query = fmt.Sprintf(
611618
"REVOKE ALL PRIVILEGES ON FOREIGN DATA WRAPPER %s FROM %s",
612619
pq.QuoteIdentifier(fdwName.(string)),
613-
pq.QuoteIdentifier(d.Get("role").(string)),
620+
pq.QuoteIdentifier(getter("role").(string)),
614621
)
615622
case "FOREIGN_SERVER":
616-
srvName := d.Get("objects").(*schema.Set).List()[0]
623+
srvName := getter("objects").(*schema.Set).List()[0]
617624
query = fmt.Sprintf(
618625
"REVOKE ALL PRIVILEGES ON FOREIGN SERVER %s FROM %s",
619626
pq.QuoteIdentifier(srvName.(string)),
620-
pq.QuoteIdentifier(d.Get("role").(string)),
627+
pq.QuoteIdentifier(getter("role").(string)),
621628
)
622629
case "COLUMN":
623-
objects := d.Get("objects").(*schema.Set)
624-
columns := d.Get("columns").(*schema.Set)
625-
privileges := d.Get("privileges").(*schema.Set)
630+
objects := getter("objects").(*schema.Set)
631+
columns := getter("columns").(*schema.Set)
632+
privileges := getter("privileges").(*schema.Set)
626633
if privileges.Len() == 0 || columns.Len() == 0 {
627634
// No privileges to revoke, so don't revoke anything
628635
query = "SELECT NULL"
@@ -631,38 +638,38 @@ func createRevokeQuery(d *schema.ResourceData) string {
631638
"REVOKE %s (%s) ON TABLE %s FROM %s",
632639
setToPgIdentSimpleList(privileges),
633640
setToPgIdentListWithoutSchema(columns),
634-
setToPgIdentList(d.Get("schema").(string), objects),
635-
pq.QuoteIdentifier(d.Get("role").(string)),
641+
setToPgIdentList(getter("schema").(string), objects),
642+
pq.QuoteIdentifier(getter("role").(string)),
636643
)
637644
}
638645
case "TABLE", "SEQUENCE", "FUNCTION", "PROCEDURE", "ROUTINE":
639-
objects := d.Get("objects").(*schema.Set)
640-
privileges := d.Get("privileges").(*schema.Set)
646+
objects := getter("objects").(*schema.Set)
647+
privileges := getter("privileges").(*schema.Set)
641648
if objects.Len() > 0 {
642649
if privileges.Len() > 0 {
643650
// Revoking specific privileges instead of all privileges
644651
// to avoid messing with column level grants
645652
query = fmt.Sprintf(
646653
"REVOKE %s ON %s %s FROM %s",
647654
setToPgIdentSimpleList(privileges),
648-
strings.ToUpper(d.Get("object_type").(string)),
649-
setToPgIdentList(d.Get("schema").(string), objects),
650-
pq.QuoteIdentifier(d.Get("role").(string)),
655+
strings.ToUpper(getter("object_type").(string)),
656+
setToPgIdentList(getter("schema").(string), objects),
657+
pq.QuoteIdentifier(getter("role").(string)),
651658
)
652659
} else {
653660
query = fmt.Sprintf(
654661
"REVOKE ALL PRIVILEGES ON %s %s FROM %s",
655-
strings.ToUpper(d.Get("object_type").(string)),
656-
setToPgIdentList(d.Get("schema").(string), objects),
657-
pq.QuoteIdentifier(d.Get("role").(string)),
662+
strings.ToUpper(getter("object_type").(string)),
663+
setToPgIdentList(getter("schema").(string), objects),
664+
pq.QuoteIdentifier(getter("role").(string)),
658665
)
659666
}
660667
} else {
661668
query = fmt.Sprintf(
662669
"REVOKE ALL PRIVILEGES ON ALL %sS IN SCHEMA %s FROM %s",
663-
strings.ToUpper(d.Get("object_type").(string)),
664-
pq.QuoteIdentifier(d.Get("schema").(string)),
665-
pq.QuoteIdentifier(d.Get("role").(string)),
670+
strings.ToUpper(getter("object_type").(string)),
671+
pq.QuoteIdentifier(getter("schema").(string)),
672+
pq.QuoteIdentifier(getter("role").(string)),
666673
)
667674
}
668675
}
@@ -687,8 +694,21 @@ func grantRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
687694
return err
688695
}
689696

690-
func revokeRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
691-
query := createRevokeQuery(d)
697+
func revokeRolePrivileges(txn *sql.Tx, d *schema.ResourceData, usePrevious bool) error {
698+
getter := d.Get
699+
700+
if usePrevious {
701+
getter = func(name string) interface{} {
702+
if d.HasChange(name) {
703+
old, _ := d.GetChange(name)
704+
return old
705+
}
706+
707+
return d.Get(name)
708+
}
709+
}
710+
711+
query := createRevokeQuery(getter)
692712
if len(query) == 0 {
693713
// Query is empty, don't run anything
694714
return nil

postgresql/resource_postgresql_grant_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ func TestCreateRevokeQuery(t *testing.T) {
293293
}
294294

295295
for _, c := range cases {
296-
out := createRevokeQuery(c.resource)
296+
out := createRevokeQuery(c.resource.Get)
297297
if out != c.expected {
298298
t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected)
299299
}

0 commit comments

Comments
 (0)