Skip to content
This repository was archived by the owner on Nov 14, 2020. It is now read-only.

Commit c05cf9e

Browse files
committed
Implement grants on database
1 parent 207a920 commit c05cf9e

File tree

4 files changed

+267
-22
lines changed

4 files changed

+267
-22
lines changed

postgresql/helpers.go

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func sliceContainsStr(haystack []string, needle string) bool {
105105
// allowedPrivileges is the list of privileges allowed per object types in Postgres.
106106
// see: https://www.postgresql.org/docs/current/sql-grant.html
107107
var allowedPrivileges = map[string][]string{
108+
"database": []string{"ALL", "CREATE", "CONNECT", "TEMPORARY", "TEMP"},
108109
"table": []string{"ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"},
109110
"sequence": []string{"ALL", "USAGE", "SELECT", "UPDATE"},
110111
}

postgresql/resource_postgresql_grant.go

+72-22
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@ import (
1515
)
1616

1717
var objectTypes = map[string]string{
18+
"database": "d",
1819
"table": "r",
1920
"sequence": "S",
2021
}
2122

2223
func resourcePostgreSQLGrant() *schema.Resource {
24+
25+
allowedObjectTypes := make([]string, 0, len(objectTypes))
26+
27+
for k := range objectTypes {
28+
allowedObjectTypes = append(allowedObjectTypes, k)
29+
}
30+
2331
return &schema.Resource{
2432
Create: resourcePostgreSQLGrantCreate,
2533
// As create revokes and grants we can use it to update too
@@ -47,14 +55,11 @@ func resourcePostgreSQLGrant() *schema.Resource {
4755
Description: "The database schema to grant privileges on for this role",
4856
},
4957
"object_type": {
50-
Type: schema.TypeString,
51-
Required: true,
52-
ForceNew: true,
53-
ValidateFunc: validation.StringInSlice([]string{
54-
"table",
55-
"sequence",
56-
}, false),
57-
Description: "The PostgreSQL object type to grant the privileges on (one of: table, sequence)",
58+
Type: schema.TypeString,
59+
Required: true,
60+
ForceNew: true,
61+
ValidateFunc: validation.StringInSlice(allowedObjectTypes, false),
62+
Description: "The PostgreSQL object type to grant the privileges on (one of: " + strings.Join(allowedObjectTypes, ", ") + ")",
5863
},
5964
"privileges": &schema.Schema{
6065
Type: schema.TypeSet,
@@ -64,6 +69,13 @@ func resourcePostgreSQLGrant() *schema.Resource {
6469
MinItems: 1,
6570
Description: "The list of privileges to grant",
6671
},
72+
"with_grant_option": {
73+
Type: schema.TypeBool,
74+
Optional: true,
75+
ForceNew: true,
76+
Default: false,
77+
Description: "Permit the grant recipient to grant it to others",
78+
},
6779
},
6880
}
6981
}
@@ -236,32 +248,70 @@ GROUP BY pg_class.relname;
236248
return nil
237249
}
238250

251+
func createGrantQuery(d *schema.ResourceData, privileges []string) string {
252+
var query string
253+
254+
switch strings.ToUpper(d.Get("object_type").(string)) {
255+
case "DATABASE":
256+
query = fmt.Sprintf(
257+
"GRANT %s ON DATABASE %s TO %s",
258+
strings.Join(privileges, ","),
259+
pq.QuoteIdentifier(d.Get("database").(string)),
260+
pq.QuoteIdentifier(d.Get("role").(string)),
261+
)
262+
case "TABLE", "SEQUENCE":
263+
query = fmt.Sprintf(
264+
"GRANT %s ON ALL %sS IN SCHEMA %s TO %s",
265+
strings.Join(privileges, ","),
266+
strings.ToUpper(d.Get("object_type").(string)),
267+
pq.QuoteIdentifier(d.Get("schema").(string)),
268+
pq.QuoteIdentifier(d.Get("role").(string)),
269+
)
270+
}
271+
272+
if d.Get("with_grant_option").(bool) == true {
273+
query = query + " WITH GRANT OPTION"
274+
}
275+
276+
return query
277+
}
278+
279+
func createRevokeQuery(d *schema.ResourceData) string {
280+
var query string
281+
282+
switch strings.ToUpper(d.Get("object_type").(string)) {
283+
case "DATABASE":
284+
query = fmt.Sprintf(
285+
"REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s",
286+
pq.QuoteIdentifier(d.Get("database").(string)),
287+
pq.QuoteIdentifier(d.Get("role").(string)),
288+
)
289+
case "TABLE", "SEQUENCE":
290+
query = fmt.Sprintf(
291+
"REVOKE ALL PRIVILEGES ON ALL %sS IN SCHEMA %s FROM %s",
292+
strings.ToUpper(d.Get("object_type").(string)),
293+
pq.QuoteIdentifier(d.Get("schema").(string)),
294+
pq.QuoteIdentifier(d.Get("role").(string)),
295+
)
296+
}
297+
298+
return query
299+
}
300+
239301
func grantRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
240302
privileges := []string{}
241303
for _, priv := range d.Get("privileges").(*schema.Set).List() {
242304
privileges = append(privileges, priv.(string))
243305
}
244306

245-
query := fmt.Sprintf(
246-
"GRANT %s ON ALL %sS IN SCHEMA %s TO %s",
247-
strings.Join(privileges, ","),
248-
strings.ToUpper(d.Get("object_type").(string)),
249-
pq.QuoteIdentifier(d.Get("schema").(string)),
250-
pq.QuoteIdentifier(d.Get("role").(string)),
251-
)
307+
query := createGrantQuery(d, privileges)
252308

253309
_, err := txn.Exec(query)
254310
return err
255311
}
256312

257313
func revokeRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
258-
query := fmt.Sprintf(
259-
"REVOKE ALL PRIVILEGES ON ALL %sS IN SCHEMA %s FROM %s",
260-
strings.ToUpper(d.Get("object_type").(string)),
261-
pq.QuoteIdentifier(d.Get("schema").(string)),
262-
pq.QuoteIdentifier(d.Get("role").(string)),
263-
)
264-
314+
query := createRevokeQuery(d)
265315
_, err := txn.Exec(query)
266316
return err
267317
}

postgresql/resource_postgresql_grant_test.go

+169
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,136 @@ import (
55
"testing"
66

77
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
8+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
89
"github.com/hashicorp/terraform-plugin-sdk/terraform"
10+
"github.com/lib/pq"
911
)
1012

13+
func TestCreateGrantQuery(t *testing.T) {
14+
var databaseName = "foo"
15+
var roleName = "bar"
16+
17+
cases := []struct {
18+
resource *schema.ResourceData
19+
privileges []string
20+
expected string
21+
}{
22+
{
23+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
24+
"object_type": "table",
25+
"schema": databaseName,
26+
"role": roleName,
27+
}),
28+
privileges: []string{"SELECT"},
29+
expected: fmt.Sprintf("GRANT SELECT ON ALL TABLES IN SCHEMA %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
30+
},
31+
{
32+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
33+
"object_type": "sequence",
34+
"schema": databaseName,
35+
"role": roleName,
36+
}),
37+
privileges: []string{"SELECT"},
38+
expected: fmt.Sprintf("GRANT SELECT ON ALL SEQUENCES IN SCHEMA %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
39+
},
40+
{
41+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
42+
"object_type": "TABLE",
43+
"schema": databaseName,
44+
"role": roleName,
45+
"with_grant_option": true,
46+
}),
47+
privileges: []string{"SELECT", "INSERT", "UPDATE"},
48+
expected: fmt.Sprintf("GRANT SELECT,INSERT,UPDATE ON ALL TABLES IN SCHEMA %s TO %s WITH GRANT OPTION", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
49+
},
50+
{
51+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
52+
"object_type": "database",
53+
"database": databaseName,
54+
"role": roleName,
55+
}),
56+
privileges: []string{"CREATE"},
57+
expected: fmt.Sprintf("GRANT CREATE ON DATABASE %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
58+
},
59+
{
60+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
61+
"object_type": "database",
62+
"database": databaseName,
63+
"role": roleName,
64+
}),
65+
privileges: []string{"CREATE", "CONNECT"},
66+
expected: fmt.Sprintf("GRANT CREATE,CONNECT ON DATABASE %s TO %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
67+
},
68+
{
69+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
70+
"object_type": "DATABASE",
71+
"database": databaseName,
72+
"role": roleName,
73+
"with_grant_option": true,
74+
}),
75+
privileges: []string{"ALL PRIVILEGES"},
76+
expected: fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s WITH GRANT OPTION", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
77+
},
78+
}
79+
80+
for _, c := range cases {
81+
out := createGrantQuery(c.resource, c.privileges)
82+
if out != c.expected {
83+
t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected)
84+
}
85+
}
86+
}
87+
88+
func TestCreateRevokeQuery(t *testing.T) {
89+
var databaseName = "foo"
90+
var roleName = "bar"
91+
92+
cases := []struct {
93+
resource *schema.ResourceData
94+
expected string
95+
}{
96+
{
97+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
98+
"object_type": "table",
99+
"schema": databaseName,
100+
"role": roleName,
101+
}),
102+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
103+
},
104+
{
105+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
106+
"object_type": "sequence",
107+
"schema": databaseName,
108+
"role": roleName,
109+
}),
110+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
111+
},
112+
{
113+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
114+
"object_type": "database",
115+
"database": databaseName,
116+
"role": roleName,
117+
}),
118+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
119+
},
120+
{
121+
resource: schema.TestResourceDataRaw(t, resourcePostgreSQLGrant().Schema, map[string]interface{}{
122+
"object_type": "DATABASE",
123+
"database": databaseName,
124+
"role": roleName,
125+
}),
126+
expected: fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", pq.QuoteIdentifier(databaseName), pq.QuoteIdentifier(roleName)),
127+
},
128+
}
129+
130+
for _, c := range cases {
131+
out := createRevokeQuery(c.resource)
132+
if out != c.expected {
133+
t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected)
134+
}
135+
}
136+
}
137+
11138
func TestAccPostgresqlGrant(t *testing.T) {
12139
skipIfNotAcc(t)
13140

@@ -84,3 +211,45 @@ func TestAccPostgresqlGrant(t *testing.T) {
84211
},
85212
})
86213
}
214+
215+
func TestAccPostgresqlGrantDatabase(t *testing.T) {
216+
skipIfNotAcc(t)
217+
218+
// We have to create the database outside of resource.Test
219+
// because we need to create tables to assert that grant are correctly applied
220+
// and we don't have this resource yet
221+
dbSuffix, teardown := setupTestDatabase(t, true, true)
222+
defer teardown()
223+
224+
dbName, roleName := getTestDBNames(dbSuffix)
225+
var testGrantSelect = fmt.Sprintf(`
226+
resource "postgresql_grant" "test" {
227+
database = "%s"
228+
role = "%s"
229+
schema = "test_schema"
230+
object_type = "database"
231+
privileges = ["CONNECT", "CREATE"]
232+
with_grant_option = true
233+
}
234+
`, dbName, roleName)
235+
236+
resource.Test(t, resource.TestCase{
237+
PreCheck: func() {
238+
testAccPreCheck(t)
239+
testCheckCompatibleVersion(t, featurePrivileges)
240+
},
241+
Providers: testAccProviders,
242+
Steps: []resource.TestStep{
243+
{
244+
Config: testGrantSelect,
245+
Check: resource.ComposeTestCheckFunc(
246+
resource.TestCheckResourceAttr("postgresql_grant.test", "privileges.#", "2"),
247+
resource.TestCheckResourceAttr("postgresql_grant.test", "with_grant_option", "true"),
248+
func(*terraform.State) error {
249+
return testCheckDatabasesPrivileges(t, dbSuffix, []string{"CONNECT"})
250+
},
251+
),
252+
},
253+
},
254+
})
255+
}

postgresql/utils_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,28 @@ func testCheckTablesPrivileges(t *testing.T, dbSuffix string, tables []string, a
192192
}
193193
return nil
194194
}
195+
196+
func testCheckDatabasesPrivileges(t *testing.T, dbSuffix string, allowedPrivileges []string) error {
197+
config := getTestConfig(t)
198+
dbName, roleName := getTestDBNames(dbSuffix)
199+
200+
// Connect as the test role
201+
config.Username = roleName
202+
config.Password = testRolePassword
203+
204+
db, err := sql.Open("postgres", config.connStr(dbName))
205+
if err != nil {
206+
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
207+
}
208+
defer db.Close()
209+
210+
queries := map[string]string{
211+
"CREATE": fmt.Sprintf("CREATE DATABASE foo"),
212+
}
213+
214+
for _, query := range queries {
215+
db.Exec(query)
216+
}
217+
218+
return nil
219+
}

0 commit comments

Comments
 (0)