Skip to content

Commit ee49cab

Browse files
postgresql_grant support for procedures and routines (#128)
* add postgresql_grant support for procedures/routines * update postgresql_grant docs to add procedures/routines * check version for procedures/routines in tests * move compatibility test before creating objects * fixup! Merge remote-tracking branch 'origin/master' into feature/grant-procedures-and-routines Co-authored-by: Cyril Gaudin <cyril.gaudin@gmail.com>
1 parent d57caf8 commit ee49cab

File tree

7 files changed

+245
-187
lines changed

7 files changed

+245
-187
lines changed

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ require (
2020
github.com/agext/levenshtein v1.2.2 // indirect
2121
github.com/apparentlymart/go-textseg v1.0.0 // indirect
2222
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
23+
github.com/aws/aws-sdk-go-v2/credentials v1.4.0 // indirect
24+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0 // indirect
25+
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2 // indirect
26+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0 // indirect
27+
github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect
28+
github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect
29+
github.com/aws/smithy-go v1.8.0 // indirect
2330
github.com/davecgh/go-spew v1.1.1 // indirect
2431
github.com/fatih/color v1.7.0 // indirect
2532
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect

go.sum

Lines changed: 0 additions & 159 deletions
Large diffs are not rendered by default.

postgresql/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const (
2929
featureReplication
3030
featureExtension
3131
featurePrivileges
32+
featureProcedure
33+
featureRoutine
3234
featurePrivilegesOnSchemas
3335
featureForceDropDatabase
3436
featurePid
@@ -68,6 +70,11 @@ var (
6870
// for Postgresql < 9.
6971
featurePrivileges: semver.MustParseRange(">=9.0.0"),
7072

73+
// Object PROCEDURE support
74+
featureProcedure: semver.MustParseRange(">=11.0.0"),
75+
76+
// Object ROUTINE support
77+
featureRoutine: semver.MustParseRange(">=11.0.0"),
7178
// ALTER DEFAULT PRIVILEGES has ON SCHEMAS support
7279
// for Postgresql >= 10
7380
featurePrivilegesOnSchemas: semver.MustParseRange(">=10.0.0"),

postgresql/helpers.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,16 @@ func sliceContainsStr(haystack []string, needle string) bool {
235235
// allowedPrivileges is the list of privileges allowed per object types in Postgres.
236236
// see: https://www.postgresql.org/docs/current/sql-grant.html
237237
var allowedPrivileges = map[string][]string{
238-
"database": []string{"ALL", "CREATE", "CONNECT", "TEMPORARY", "TEMP"},
239-
"table": []string{"ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"},
240-
"sequence": []string{"ALL", "USAGE", "SELECT", "UPDATE"},
241-
"schema": []string{"ALL", "CREATE", "USAGE"},
242-
"function": []string{"ALL", "EXECUTE"},
243-
"type": []string{"ALL", "USAGE"},
244-
"foreign_data_wrapper": []string{"ALL", "USAGE"},
245-
"foreign_server": []string{"ALL", "USAGE"},
238+
"database": {"ALL", "CREATE", "CONNECT", "TEMPORARY", "TEMP"},
239+
"table": {"ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"},
240+
"sequence": {"ALL", "USAGE", "SELECT", "UPDATE"},
241+
"schema": {"ALL", "CREATE", "USAGE"},
242+
"function": {"ALL", "EXECUTE"},
243+
"procedure": {"ALL", "EXECUTE"},
244+
"routine": {"ALL", "EXECUTE"},
245+
"type": {"ALL", "USAGE"},
246+
"foreign_data_wrapper": {"ALL", "USAGE"},
247+
"foreign_server": {"ALL", "USAGE"},
246248
}
247249

248250
// validatePrivileges checks that privileges to apply are allowed for this object type.

postgresql/resource_postgresql_grant.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
var allowedObjectTypes = []string{
1717
"database",
1818
"function",
19+
"procedure",
20+
"routine",
1921
"schema",
2022
"sequence",
2123
"table",
@@ -73,7 +75,7 @@ func resourcePostgreSQLGrant() *schema.Resource {
7375
Set: schema.HashString,
7476
Description: "The specific objects to grant privileges on for this role (empty means all objects of the requested type)",
7577
},
76-
"privileges": &schema.Schema{
78+
"privileges": {
7779
Type: schema.TypeSet,
7880
Required: true,
7981
Elem: &schema.Schema{Type: schema.TypeString},
@@ -92,11 +94,8 @@ func resourcePostgreSQLGrant() *schema.Resource {
9294
}
9395

9496
func resourcePostgreSQLGrantRead(db *DBConnection, d *schema.ResourceData) error {
95-
if !db.featureSupported(featurePrivileges) {
96-
return fmt.Errorf(
97-
"postgresql_grant resource is not supported for this Postgres version (%s)",
98-
db.version,
99-
)
97+
if err := validateFeatureSupport(db, d); err != nil {
98+
return fmt.Errorf("feature is not supported: %v", err)
10099
}
101100

102101
exists, err := checkRoleDBSchemaExists(db.client, d)
@@ -119,11 +118,8 @@ func resourcePostgreSQLGrantRead(db *DBConnection, d *schema.ResourceData) error
119118
}
120119

121120
func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) error {
122-
if !db.featureSupported(featurePrivileges) {
123-
return fmt.Errorf(
124-
"postgresql_grant resource is not supported for this Postgres version (%s)",
125-
db.version,
126-
)
121+
if err := validateFeatureSupport(db, d); err != nil {
122+
return fmt.Errorf("feature is not supported: %v", err)
127123
}
128124

129125
// Validate parameters.
@@ -189,11 +185,8 @@ func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) err
189185
}
190186

191187
func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) error {
192-
if !db.featureSupported(featurePrivileges) {
193-
return fmt.Errorf(
194-
"postgresql_grant resource is not supported for this Postgres version (%s)",
195-
db.version,
196-
)
188+
if err := validateFeatureSupport(db, d); err != nil {
189+
return fmt.Errorf("feature is not supported: %v", err)
197190
}
198191

199192
txn, err := startTransaction(db.client, d.Get("database").(string))
@@ -329,7 +322,7 @@ func readRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
329322
case "foreign_server":
330323
return readForeignServerRolePrivileges(txn, d, roleOID)
331324

332-
case "function":
325+
case "function", "procedure", "routine":
333326
query = `
334327
SELECT pg_proc.proname, array_remove(array_agg(privilege_type), NULL)
335328
FROM pg_proc
@@ -441,7 +434,7 @@ func createGrantQuery(d *schema.ResourceData, privileges []string) string {
441434
pq.QuoteIdentifier(srvName.(string)),
442435
pq.QuoteIdentifier(d.Get("role").(string)),
443436
)
444-
case "TABLE", "SEQUENCE", "FUNCTION":
437+
case "TABLE", "SEQUENCE", "FUNCTION", "PROCEDURE", "ROUTINE":
445438
objects := d.Get("objects").(*schema.Set)
446439
if objects.Len() > 0 {
447440
query = fmt.Sprintf(
@@ -499,7 +492,7 @@ func createRevokeQuery(d *schema.ResourceData) string {
499492
pq.QuoteIdentifier(srvName.(string)),
500493
pq.QuoteIdentifier(d.Get("role").(string)),
501494
)
502-
case "TABLE", "SEQUENCE", "FUNCTION":
495+
case "TABLE", "SEQUENCE", "FUNCTION", "PROCEDURE", "ROUTINE":
503496
objects := d.Get("objects").(*schema.Set)
504497
if objects.Len() > 0 {
505498
query = fmt.Sprintf(
@@ -648,3 +641,25 @@ func getRolesToGrant(txn *sql.Tx, d *schema.ResourceData) ([]string, error) {
648641

649642
return owners, nil
650643
}
644+
645+
func validateFeatureSupport(db *DBConnection, d *schema.ResourceData) error {
646+
if !db.featureSupported(featurePrivileges) {
647+
return fmt.Errorf(
648+
"postgresql_grant resource is not supported for this Postgres version (%s)",
649+
db.version,
650+
)
651+
}
652+
if d.Get("object_type") == "procedure" && !db.featureSupported(featureProcedure) {
653+
return fmt.Errorf(
654+
"object type PROCEDURE is not supported for this Postgres version (%s)",
655+
db.version,
656+
)
657+
}
658+
if d.Get("object_type") == "routine" && !db.featureSupported(featureRoutine) {
659+
return fmt.Errorf(
660+
"object type ROUTINE is not supported for this Postgres version (%s)",
661+
db.version,
662+
)
663+
}
664+
return nil
665+
}

postgresql/resource_postgresql_grant_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ func TestCreateGrantQuery(t *testing.T) {
4949
privileges: []string{"EXECUTE"},
5050
expected: fmt.Sprintf("GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
5151
},
52+
{
53+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
54+
"object_type": "procedure",
55+
"schema": databaseName,
56+
"role": roleName,
57+
}),
58+
privileges: []string{"EXECUTE"},
59+
expected: fmt.Sprintf("GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
60+
},
61+
{
62+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
63+
"object_type": "routine",
64+
"schema": databaseName,
65+
"role": roleName,
66+
}),
67+
privileges: []string{"EXECUTE"},
68+
expected: fmt.Sprintf("GRANT EXECUTE ON ALL ROUTINES IN SCHEMA %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
69+
},
5270
{
5371
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
5472
"object_type": "TABLE",
@@ -171,6 +189,30 @@ func TestCreateRevokeQuery(t *testing.T) {
171189
}),
172190
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
173191
},
192+
{
193+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
194+
"object_type": "function",
195+
"schema": databaseName,
196+
"role": roleName,
197+
}),
198+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
199+
},
200+
{
201+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
202+
"object_type": "procedure",
203+
"schema": databaseName,
204+
"role": roleName,
205+
}),
206+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL PROCEDURES IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
207+
},
208+
{
209+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
210+
"object_type": "routine",
211+
"schema": databaseName,
212+
"role": roleName,
213+
}),
214+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL ROUTINES IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
215+
},
174216
{
175217
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
176218
"object_type": "database",
@@ -608,6 +650,7 @@ func TestAccPostgresqlGrantFunction(t *testing.T) {
608650
dbExecute(t, dsn, fmt.Sprintf("CREATE ROLE test_role LOGIN PASSWORD '%s'", testRolePassword))
609651
dbExecute(t, dsn, "CREATE SCHEMA test_schema")
610652
dbExecute(t, dsn, "GRANT USAGE ON SCHEMA test_schema TO test_role")
653+
dbExecute(t, dsn, "ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC")
611654

612655
// Create test function in this schema
613656
dbExecute(t, dsn, `
@@ -658,6 +701,137 @@ resource postgresql_grant "test" {
658701
}
659702
}
660703

704+
func TestAccPostgresqlGrantProcedure(t *testing.T) {
705+
skipIfNotAcc(t)
706+
testCheckCompatibleVersion(t, featureProcedure)
707+
708+
config := getTestConfig(t)
709+
dsn := config.connStr("postgres")
710+
711+
// Create a test role and a schema as public has too wide open privileges
712+
dbExecute(t, dsn, fmt.Sprintf("CREATE ROLE test_role LOGIN PASSWORD '%s'", testRolePassword))
713+
dbExecute(t, dsn, "CREATE SCHEMA test_schema")
714+
dbExecute(t, dsn, "GRANT USAGE ON SCHEMA test_schema TO test_role")
715+
dbExecute(t, dsn, "ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC")
716+
717+
// Create test procedure in this schema
718+
dbExecute(t, dsn, `
719+
CREATE PROCEDURE test_schema.test()
720+
AS $$ select 'foo'::text $$
721+
LANGUAGE SQL;
722+
`)
723+
defer func() {
724+
dbExecute(t, dsn, "DROP SCHEMA test_schema CASCADE")
725+
dbExecute(t, dsn, "DROP ROLE test_role")
726+
}()
727+
728+
// Test to grant directly to test_role and to public
729+
// in both case test_case should have the right
730+
for _, role := range []string{"test_role", "public"} {
731+
t.Run(role, func(t *testing.T) {
732+
733+
tfConfig := fmt.Sprintf(`
734+
resource postgresql_grant "test" {
735+
database = "postgres"
736+
role = "%s"
737+
schema = "test_schema"
738+
object_type = "procedure"
739+
privileges = ["EXECUTE"]
740+
}
741+
`, role)
742+
743+
resource.Test(t, resource.TestCase{
744+
PreCheck: func() {
745+
testAccPreCheck(t)
746+
testCheckCompatibleVersion(t, featurePrivileges)
747+
},
748+
Providers: testAccProviders,
749+
Steps: []resource.TestStep{
750+
{
751+
Config: tfConfig,
752+
Check: resource.ComposeTestCheckFunc(
753+
resource.TestCheckResourceAttr("postgresql_grant.test", "id", fmt.Sprintf("%s_postgres_test_schema_procedure", role)),
754+
resource.TestCheckResourceAttr("postgresql_grant.test", "privileges.#", "1"),
755+
resource.TestCheckResourceAttr("postgresql_grant.test", "privileges.0", "EXECUTE"),
756+
resource.TestCheckResourceAttr("postgresql_grant.test", "with_grant_option", "false"),
757+
testCheckProcedureExecutable(t, "test_role", "test_schema.test"),
758+
),
759+
},
760+
},
761+
})
762+
})
763+
}
764+
}
765+
766+
func TestAccPostgresqlGrantRoutine(t *testing.T) {
767+
skipIfNotAcc(t)
768+
testCheckCompatibleVersion(t, featureRoutine)
769+
770+
config := getTestConfig(t)
771+
dsn := config.connStr("postgres")
772+
773+
// Create a test role and a schema as public has too wide open privileges
774+
dbExecute(t, dsn, fmt.Sprintf("CREATE ROLE test_role LOGIN PASSWORD '%s'", testRolePassword))
775+
dbExecute(t, dsn, "CREATE SCHEMA test_schema")
776+
dbExecute(t, dsn, "GRANT USAGE ON SCHEMA test_schema TO test_role")
777+
dbExecute(t, dsn, "ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC")
778+
779+
// Create test function in this schema
780+
dbExecute(t, dsn, `
781+
CREATE FUNCTION test_schema.test_function() RETURNS text
782+
AS $$ select 'foo'::text $$
783+
LANGUAGE SQL;
784+
`)
785+
// Create test procedure in this schema
786+
dbExecute(t, dsn, `
787+
CREATE PROCEDURE test_schema.test_procedure()
788+
AS $$ select 'foo'::text $$
789+
LANGUAGE SQL;
790+
`)
791+
defer func() {
792+
dbExecute(t, dsn, "DROP SCHEMA test_schema CASCADE")
793+
dbExecute(t, dsn, "DROP ROLE test_role")
794+
}()
795+
796+
// Test to grant directly to test_role and to public
797+
// in both case test_case should have the right
798+
for _, role := range []string{"test_role", "public"} {
799+
t.Run(role, func(t *testing.T) {
800+
801+
tfConfigRoutine := fmt.Sprintf(`
802+
resource postgresql_grant "test" {
803+
database = "postgres"
804+
role = "%s"
805+
schema = "test_schema"
806+
object_type = "routine"
807+
privileges = ["EXECUTE"]
808+
}
809+
`, role)
810+
811+
resource.Test(t, resource.TestCase{
812+
PreCheck: func() {
813+
testAccPreCheck(t)
814+
testCheckCompatibleVersion(t, featurePrivileges)
815+
},
816+
Providers: testAccProviders,
817+
Steps: []resource.TestStep{
818+
{
819+
Config: tfConfigRoutine,
820+
Check: resource.ComposeTestCheckFunc(
821+
resource.TestCheckResourceAttr("postgresql_grant.test", "id", fmt.Sprintf("%s_postgres_test_schema_routine", role)),
822+
resource.TestCheckResourceAttr("postgresql_grant.test", "privileges.#", "1"),
823+
resource.TestCheckResourceAttr("postgresql_grant.test", "privileges.0", "EXECUTE"),
824+
resource.TestCheckResourceAttr("postgresql_grant.test", "with_grant_option", "false"),
825+
testCheckFunctionExecutable(t, "test_role", "test_schema.test_function"),
826+
testCheckProcedureExecutable(t, "test_role", "test_schema.test_procedure"),
827+
),
828+
},
829+
},
830+
})
831+
})
832+
}
833+
}
834+
661835
func TestAccPostgresqlGrantDatabase(t *testing.T) {
662836
// create a TF config with placeholder for privileges
663837
// it will be filled in each step.
@@ -931,6 +1105,18 @@ func testCheckFunctionExecutable(t *testing.T, role, function string) func(*terr
9311105
}
9321106
}
9331107

1108+
func testCheckProcedureExecutable(t *testing.T, role, procedure string) func(*terraform.State) error {
1109+
return func(*terraform.State) error {
1110+
db := connectAsTestRole(t, role, "postgres")
1111+
defer db.Close()
1112+
1113+
if err := testHasGrantForQuery(db, fmt.Sprintf("CALL %s()", procedure), true); err != nil {
1114+
return err
1115+
}
1116+
return nil
1117+
}
1118+
}
1119+
9341120
func testCheckSchemaPrivileges(t *testing.T, usage, create bool) func(*terraform.State) error {
9351121
return func(*terraform.State) error {
9361122
config := getTestConfig(t)

0 commit comments

Comments
 (0)