Skip to content

Commit fc5c40d

Browse files
talbxcyrilgdnCyril Gaudin
authored
fix: ALL implicit privileges equality check (#339)
When using resources `postgresql_default_privileges` or `postgresql_grant` you have the option to define a fine grained set of privileges (for example only `CONNECT` for object_type `database`) or all at once, either by setting all privileges (for database: `["CREATE", "CONNECT", "TEMPORARY"]` or simply `["ALL"]`. The latter one is quite handy, since you do not have to find out which privileges are part of it. When using `ALL`, the provider will grant the privileges by using the set of the actual grants, so in this scenario by granting `"CREATE", "CONNECT", "TEMPORARY"` and not `ALL`. Probably because `ALL` is not known to postgres. 🤷🏼‍♂️ Anyways, when executing a `terraform plan` the next time after the apply for `ALL`, the diff will find out, that not `ALL` is set as privilege, but `"CREATE", "CONNECT", "TEMPORARY"`. Since these privileges are the same or have the same semantic to it, there shouldn't be really a update each time you use terraform, because the state on the DB is totally fine and according to the resource. To prevent this unnecessary modification, I implemented some more detailed equality check for all `object_type`s except `column`. I tested the changes locally with a dockerized postgres and also wrote some unit tests for it. Would love to hear some feedback about it, especially since it's my first time working on a terraform provider.. Cheers fix #32 --------- Co-authored-by: Cyril Gaudin <cyril.gaudin@gmail.com> Co-authored-by: Cyril Gaudin <cgaudin@edgelab.ch>
1 parent 6d26b59 commit fc5c40d

File tree

5 files changed

+203
-12
lines changed

5 files changed

+203
-12
lines changed

postgresql/helpers.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,30 @@ func validatePrivileges(d *schema.ResourceData) error {
281281
return nil
282282
}
283283

284+
func resourcePrivilegesEqual(granted *schema.Set, d *schema.ResourceData) bool {
285+
objectType := d.Get("object_type").(string)
286+
wanted := d.Get("privileges").(*schema.Set)
287+
288+
if granted.Equal(wanted) {
289+
return true
290+
}
291+
292+
if !wanted.Contains("ALL") {
293+
return false
294+
}
295+
296+
// implicit check: e.g. for object_type schema -> ALL == ["CREATE", "USAGE"]
297+
log.Printf("The wanted privilege is 'ALL'. therefore, we will check if the current privileges are ALL implicitely")
298+
implicits := []interface{}{}
299+
for _, p := range allowedPrivileges[objectType] {
300+
if p != "ALL" {
301+
implicits = append(implicits, p)
302+
}
303+
}
304+
wantedSet := schema.NewSet(schema.HashString, implicits)
305+
return granted.Equal(wantedSet)
306+
}
307+
284308
func pgArrayToSet(arr pq.ByteaArray) *schema.Set {
285309
s := make([]interface{}, len(arr))
286310
for i, v := range arr {

postgresql/helpers_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package postgresql
22

33
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -45,3 +46,78 @@ func TestQuoteTableName(t *testing.T) {
4546
})
4647
}
4748
}
49+
50+
func TestArePrivilegesEqual(t *testing.T) {
51+
52+
type PrivilegesTestObject struct {
53+
d *schema.ResourceData
54+
granted *schema.Set
55+
wanted *schema.Set
56+
assertion bool
57+
}
58+
59+
tt := []PrivilegesTestObject{
60+
{
61+
buildResourceData("database", t),
62+
buildPrivilegesSet("CONNECT", "CREATE", "TEMPORARY"),
63+
buildPrivilegesSet("ALL"),
64+
true,
65+
},
66+
{
67+
buildResourceData("database", t),
68+
buildPrivilegesSet("CREATE", "USAGE"),
69+
buildPrivilegesSet("USAGE"),
70+
false,
71+
},
72+
{
73+
buildResourceData("table", t),
74+
buildPrivilegesSet("SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"),
75+
buildPrivilegesSet("ALL"),
76+
true,
77+
},
78+
{
79+
buildResourceData("table", t),
80+
buildPrivilegesSet("SELECT"),
81+
buildPrivilegesSet("SELECT, INSERT"),
82+
false,
83+
},
84+
{
85+
buildResourceData("schema", t),
86+
buildPrivilegesSet("CREATE", "USAGE"),
87+
buildPrivilegesSet("ALL"),
88+
true,
89+
},
90+
{
91+
buildResourceData("schema", t),
92+
buildPrivilegesSet("CREATE"),
93+
buildPrivilegesSet("ALL"),
94+
false,
95+
},
96+
}
97+
98+
for _, configuration := range tt {
99+
err := configuration.d.Set("privileges", configuration.wanted)
100+
assert.NoError(t, err)
101+
equal := resourcePrivilegesEqual(configuration.granted, configuration.d)
102+
assert.Equal(t, configuration.assertion, equal)
103+
}
104+
}
105+
106+
func buildPrivilegesSet(grants ...interface{}) *schema.Set {
107+
return schema.NewSet(schema.HashString, grants)
108+
}
109+
110+
func buildResourceData(objectType string, t *testing.T) *schema.ResourceData {
111+
var testSchema = map[string]*schema.Schema{
112+
"object_type": {Type: schema.TypeString},
113+
"privileges": {
114+
Type: schema.TypeSet,
115+
Elem: &schema.Schema{Type: schema.TypeString},
116+
Set: schema.HashString,
117+
},
118+
}
119+
120+
m := make(map[string]any)
121+
m["object_type"] = objectType
122+
return schema.TestResourceDataRaw(t, testSchema, m)
123+
}

postgresql/resource_postgresql_default_privileges.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,11 @@ func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error {
268268
}
269269

270270
privilegesSet := pgArrayToSet(privileges)
271-
d.Set("privileges", privilegesSet)
271+
privilegesEqual := resourcePrivilegesEqual(privilegesSet, d)
272+
273+
if !privilegesEqual {
274+
d.Set("privileges", privilegesSet)
275+
}
272276
d.SetId(generateDefaultPrivilegesID(d))
273277

274278
return nil

postgresql/resource_postgresql_grant.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err
262262
return nil
263263
}
264264

265-
func readDatabaseRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error {
265+
func readDatabaseRolePrivileges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error {
266266
dbName := d.Get("database").(string)
267267
query := `
268268
SELECT array_agg(privilege_type)
@@ -276,12 +276,14 @@ WHERE grantee = $2
276276
if err := txn.QueryRow(query, dbName, roleOID).Scan(&privileges); err != nil {
277277
return fmt.Errorf("could not read privileges for database %s: %w", dbName, err)
278278
}
279-
280-
d.Set("privileges", pgArrayToSet(privileges))
279+
granted := pgArrayToSet(privileges)
280+
if !resourcePrivilegesEqual(granted, d) {
281+
return d.Set("privileges", granted)
282+
}
281283
return nil
282284
}
283285

284-
func readSchemaRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error {
286+
func readSchemaRolePrivileges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error {
285287
dbName := d.Get("schema").(string)
286288
query := `
287289
SELECT array_agg(privilege_type)
@@ -296,7 +298,10 @@ WHERE grantee = $2
296298
return fmt.Errorf("could not read privileges for schema %s: %w", dbName, err)
297299
}
298300

299-
d.Set("privileges", pgArrayToSet(privileges))
301+
granted := pgArrayToSet(privileges)
302+
if !resourcePrivilegesEqual(granted, d) {
303+
return d.Set("privileges", granted)
304+
}
300305
return nil
301306
}
302307

@@ -316,7 +321,10 @@ WHERE grantee = $2
316321
return fmt.Errorf("could not read privileges for foreign data wrapper %s: %w", fdwName, err)
317322
}
318323

319-
d.Set("privileges", pgArrayToSet(privileges))
324+
granted := pgArrayToSet(privileges)
325+
if !resourcePrivilegesEqual(granted, d) {
326+
return d.Set("privileges", granted)
327+
}
320328
return nil
321329
}
322330

@@ -336,7 +344,10 @@ WHERE grantee = $2
336344
return fmt.Errorf("could not read privileges for foreign server %s: %w", srvName, err)
337345
}
338346

339-
d.Set("privileges", pgArrayToSet(privileges))
347+
granted := pgArrayToSet(privileges)
348+
if !resourcePrivilegesEqual(granted, d) {
349+
return d.Set("privileges", granted)
350+
}
340351
return nil
341352
}
342353

@@ -433,10 +444,10 @@ func readRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
433444

434445
switch objectType {
435446
case "database":
436-
return readDatabaseRolePriviges(txn, d, roleOID)
447+
return readDatabaseRolePrivileges(txn, d, roleOID)
437448

438449
case "schema":
439-
return readSchemaRolePriviges(txn, d, roleOID)
450+
return readSchemaRolePrivileges(txn, d, roleOID)
440451

441452
case "foreign_data_wrapper":
442453
return readForeignDataWrapperRolePrivileges(txn, d, roleOID)
@@ -509,8 +520,7 @@ GROUP BY pg_class.relname
509520
}
510521

511522
privilegesSet := pgArrayToSet(privileges)
512-
513-
if !privilegesSet.Equal(d.Get("privileges").(*schema.Set)) {
523+
if !resourcePrivilegesEqual(privilegesSet, d) {
514524
// If any object doesn't have the same privileges as saved in the state,
515525
// we return its privileges to force an update.
516526
log.Printf(

postgresql/resource_postgresql_grant_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,83 @@ resource "postgresql_grant" "test" {
11391139
})
11401140
}
11411141

1142+
func TestAccPostgresqlImplicitGrants(t *testing.T) {
1143+
skipIfNotAcc(t)
1144+
1145+
dbSuffix, teardown := setupTestDatabase(t, true, true)
1146+
defer teardown()
1147+
1148+
testTables := []string{"test_schema.test_table"}
1149+
createTestTables(t, dbSuffix, testTables, "")
1150+
1151+
dbName, roleName := getTestDBNames(dbSuffix)
1152+
1153+
// create a TF config with placeholder for privileges
1154+
// it will be filled in each step.
1155+
var testGrant = fmt.Sprintf(`
1156+
resource "postgresql_grant" "test" {
1157+
database = "%s"
1158+
role = "%s"
1159+
schema = "test_schema"
1160+
object_type = "table"
1161+
objects = ["test_table"]
1162+
privileges = %%s
1163+
}
1164+
`, dbName, roleName)
1165+
1166+
var testCheckTableGrants = func(grants ...string) resource.TestCheckFunc {
1167+
return func(*terraform.State) error {
1168+
return testCheckTablesPrivileges(t, dbName, roleName, []string{testTables[0]}, grants)
1169+
}
1170+
}
1171+
resource.Test(t, resource.TestCase{
1172+
PreCheck: func() {
1173+
testAccPreCheck(t)
1174+
testCheckCompatibleVersion(t, featurePrivileges)
1175+
},
1176+
Providers: testAccProviders,
1177+
Steps: []resource.TestStep{
1178+
{
1179+
Config: fmt.Sprintf(testGrant, `["ALL"]`),
1180+
Check: resource.ComposeTestCheckFunc(
1181+
resource.TestCheckResourceAttr(
1182+
"postgresql_grant.test", "id", fmt.Sprintf("%s_%s_test_schema_table_test_table", roleName, dbName),
1183+
),
1184+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"),
1185+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"),
1186+
testCheckTableGrants("SELECT", "INSERT", "UPDATE", "DELETE"),
1187+
),
1188+
},
1189+
{
1190+
Config: fmt.Sprintf(testGrant, `["SELECT"]`),
1191+
Check: resource.ComposeTestCheckFunc(
1192+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"),
1193+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"),
1194+
testCheckTableGrants("SELECT"),
1195+
),
1196+
},
1197+
{
1198+
// Empty list means that privileges will be applied on all tables.
1199+
Config: fmt.Sprintf(testGrant, `["SELECT", "INSERT", "UPDATE", "DELETE"]`),
1200+
Check: resource.ComposeTestCheckFunc(
1201+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"),
1202+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"),
1203+
testCheckTableGrants("SELECT", "INSERT", "UPDATE", "DELETE"),
1204+
),
1205+
},
1206+
{
1207+
Config: fmt.Sprintf(testGrant, `[]`),
1208+
Destroy: true,
1209+
Check: resource.ComposeTestCheckFunc(
1210+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"),
1211+
resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"),
1212+
testCheckTableGrants(""),
1213+
),
1214+
},
1215+
},
1216+
})
1217+
}
1218+
11421219
func TestAccPostgresqlGrantSchema(t *testing.T) {
11431220
// create a TF config with placeholder for privileges
11441221
// it will be filled in each step.

0 commit comments

Comments
 (0)