Skip to content

Commit 20a8f4a

Browse files
author
Maximilian Schelbach
committed
create postgres_grant_resource resource
1 parent 89f50c2 commit 20a8f4a

4 files changed

+437
-0
lines changed

postgresql/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func Provider() terraform.ResourceProvider {
140140
"postgresql_default_privileges": resourcePostgreSQLDefaultPrivileges(),
141141
"postgresql_extension": resourcePostgreSQLExtension(),
142142
"postgresql_grant": resourcePostgreSQLGrant(),
143+
"postgresql_grant_resource": resourcePostgreSQLGrantResource(),
143144
"postgresql_grant_role": resourcePostgreSQLGrantRole(),
144145
"postgresql_schema": resourcePostgreSQLSchema(),
145146
"postgresql_role": resourcePostgreSQLRole(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
package postgresql
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"log"
7+
"strings"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
11+
12+
// Use Postgres as SQL driver
13+
"github.com/lib/pq"
14+
)
15+
16+
var allowedResourceObjectTypes = []string{
17+
"function",
18+
"sequence",
19+
"table",
20+
}
21+
22+
var resourceObjectTypes = map[string]string{
23+
"table": "r",
24+
"sequence": "S",
25+
"function": "f",
26+
"type": "T",
27+
}
28+
29+
func resourcePostgreSQLGrantResource() *schema.Resource {
30+
return &schema.Resource{
31+
Create: PGResourceFunc(resourcePostgreSQLGrantResourceCreate),
32+
Read: PGResourceFunc(resourcePostgreSQLGrantResourceRead),
33+
Delete: PGResourceFunc(resourcePostgreSQLGrantResourceDelete),
34+
35+
Schema: map[string]*schema.Schema{
36+
"role": {
37+
Type: schema.TypeString,
38+
Required: true,
39+
ForceNew: true,
40+
Description: "The name of the role to grant privileges on",
41+
},
42+
"database": {
43+
Type: schema.TypeString,
44+
Required: true,
45+
ForceNew: true,
46+
Description: "The database to grant privileges on for this role",
47+
},
48+
"schema": {
49+
Type: schema.TypeString,
50+
Required: true,
51+
ForceNew: true,
52+
Description: "The database schema to grant privileges on for this role",
53+
},
54+
"object_type": {
55+
Type: schema.TypeString,
56+
Required: true,
57+
ForceNew: true,
58+
ValidateFunc: validation.StringInSlice(allowedResourceObjectTypes, false),
59+
Description: "The PostgreSQL object type to grant the privileges on (one of: " + strings.Join(allowedResourceObjectTypes, ", ") + ")",
60+
},
61+
"privileges": {
62+
Type: schema.TypeSet,
63+
Required: true,
64+
Elem: &schema.Schema{Type: schema.TypeString},
65+
Set: schema.HashString,
66+
ForceNew: true,
67+
Description: "The list of privileges to grant",
68+
},
69+
"resources": {
70+
Type: schema.TypeSet,
71+
Required: true,
72+
Elem: &schema.Schema{Type: schema.TypeString},
73+
Set: schema.HashString,
74+
ForceNew: true,
75+
MinItems: 1,
76+
Description: "The name of the object, on which the grant should be applied on",
77+
},
78+
"with_grant_option": {
79+
Type: schema.TypeBool,
80+
Optional: true,
81+
ForceNew: true,
82+
Default: false,
83+
Description: "Permit the grant recipient to grant it to others",
84+
},
85+
},
86+
}
87+
}
88+
89+
func resourcePostgreSQLGrantResourceRead(db *DBConnection, d *schema.ResourceData) error {
90+
if !db.featureSupported(featurePrivileges) {
91+
return fmt.Errorf(
92+
"postgresql_grant_resource resource is not supported for this Postgres version (%s)",
93+
db.version,
94+
)
95+
}
96+
97+
exists, err := checkRoleDBSchemaExists(db.client, d)
98+
if err != nil {
99+
return err
100+
}
101+
if !exists {
102+
d.SetId("")
103+
return nil
104+
}
105+
d.SetId(generateGrantID(d))
106+
107+
txn, err := startTransaction(db.client, d.Get("database").(string))
108+
if err != nil {
109+
return err
110+
}
111+
defer deferredRollback(txn)
112+
113+
return readRoleResourcePrivileges(txn, d)
114+
}
115+
116+
func resourcePostgreSQLGrantResourceCreate(db *DBConnection, d *schema.ResourceData) error {
117+
if !db.featureSupported(featurePrivileges) {
118+
return fmt.Errorf(
119+
"postgresql_grant_resource resource is not supported for this Postgres version (%s)",
120+
db.version,
121+
)
122+
}
123+
124+
if err := validatePrivileges(d); err != nil {
125+
return err
126+
}
127+
128+
database := d.Get("database").(string)
129+
130+
txn, err := startTransaction(db.client, database)
131+
if err != nil {
132+
return err
133+
}
134+
defer deferredRollback(txn)
135+
136+
owners, err := getRolesToGrant(txn, d)
137+
if err != nil {
138+
return err
139+
}
140+
if err := withRolesGranted(txn, owners, func() error {
141+
// Revoke all privileges before granting otherwise reducing privileges will not work.
142+
// We just have to revoke them in the same transaction so the role will not lost its
143+
// privileges between the revoke and grant statements.
144+
if err := revokeRoleResourcePrivileges(txn, d); err != nil {
145+
return err
146+
}
147+
if err := grantRoleResourcePrivileges(txn, d); err != nil {
148+
return err
149+
}
150+
return nil
151+
}); err != nil {
152+
return err
153+
}
154+
155+
if err = txn.Commit(); err != nil {
156+
return fmt.Errorf("could not commit transaction: %w", err)
157+
}
158+
159+
d.SetId(generateGrantID(d))
160+
161+
txn, err = startTransaction(db.client, database)
162+
if err != nil {
163+
return err
164+
}
165+
defer deferredRollback(txn)
166+
167+
return readRoleResourcePrivileges(txn, d)
168+
}
169+
170+
func resourcePostgreSQLGrantResourceDelete(db *DBConnection, d *schema.ResourceData) error {
171+
if !db.featureSupported(featurePrivileges) {
172+
return fmt.Errorf(
173+
"postgresql_grant_resource resource is not supported for this Postgres version (%s)",
174+
db.version,
175+
)
176+
}
177+
178+
txn, err := startTransaction(db.client, d.Get("database").(string))
179+
if err != nil {
180+
return err
181+
}
182+
defer deferredRollback(txn)
183+
184+
owners, err := getRolesToGrant(txn, d)
185+
if err != nil {
186+
return err
187+
}
188+
189+
if err := withRolesGranted(txn, owners, func() error {
190+
return revokeRoleResourcePrivileges(txn, d)
191+
}); err != nil {
192+
return err
193+
}
194+
195+
if err = txn.Commit(); err != nil {
196+
return fmt.Errorf("could not commit transaction: %w", err)
197+
}
198+
199+
return nil
200+
}
201+
202+
func readRoleResourcePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
203+
role := d.Get("role").(string)
204+
objectType := d.Get("object_type").(string)
205+
206+
roleOID, err := getRoleOID(txn, role)
207+
if err != nil {
208+
return err
209+
}
210+
211+
var query string
212+
var rows *sql.Rows
213+
214+
var resourcesList []string
215+
for _, priv := range d.Get("resources").(*schema.Set).List() {
216+
resourcesList = append(resourcesList, pq.QuoteLiteral(priv.(string)))
217+
}
218+
219+
if objectType == "function" {
220+
query = fmt.Sprintf(`
221+
SELECT pg_proc.proname, array_remove(array_agg(privilege_type), NULL)
222+
FROM pg_proc
223+
JOIN pg_namespace ON pg_namespace.oid = pg_proc.pronamespace
224+
LEFT JOIN (
225+
SELECT acls.*
226+
FROM (
227+
SELECT proname, pronamespace, (aclexplode(proacl)).* FROM pg_proc
228+
) acls
229+
WHERE grantee = $1
230+
) privs
231+
USING (proname, pronamespace)
232+
WHERE nspname = $2 AND proname IN (%s)
233+
GROUP BY pg_proc.proname
234+
`, strings.Join(resourcesList, ","))
235+
rows, err = txn.Query(
236+
query, roleOID, d.Get("schema"),
237+
)
238+
} else {
239+
query = fmt.Sprintf(`
240+
SELECT pg_class.relname, array_remove(array_agg(privilege_type), NULL)
241+
FROM pg_class
242+
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
243+
LEFT JOIN (
244+
SELECT acls.* FROM (
245+
SELECT relname, relnamespace, relkind, (aclexplode(relacl)).* FROM pg_class C
246+
) AS acls
247+
WHERE grantee=$1
248+
) privs
249+
USING (relname, relnamespace, relkind)
250+
WHERE nspname = $2 AND relkind = $3 AND relname IN (%s)
251+
GROUP BY pg_class.relname
252+
`, strings.Join(resourcesList, ","))
253+
rows, err = txn.Query(
254+
query, roleOID, d.Get("schema"), resourceObjectTypes[objectType],
255+
)
256+
}
257+
258+
// This returns, for the specified role (rolname),
259+
// the list of all object of the specified type (relkind) in the specified schema (namespace)
260+
// with the list of the currently applied privileges (aggregation of privilege_type)
261+
//
262+
// Our goal is to check that every object has the same privileges as saved in the state.
263+
if err != nil {
264+
return err
265+
}
266+
267+
for rows.Next() {
268+
var objName string
269+
var privileges pq.ByteaArray
270+
271+
if err := rows.Scan(&objName, &privileges); err != nil {
272+
return err
273+
}
274+
privilegesSet := pgArrayToSet(privileges)
275+
276+
if !privilegesSet.Equal(d.Get("privileges").(*schema.Set)) {
277+
// If any object doesn't have the same privileges as saved in the state,
278+
// we return its privileges to force an update.
279+
log.Printf(
280+
"[DEBUG] %s %s has not the expected privileges %v for role %s",
281+
strings.ToTitle(objectType), objName, privileges, d.Get("role"),
282+
)
283+
d.Set("privileges", privilegesSet)
284+
break
285+
}
286+
}
287+
288+
return nil
289+
}
290+
291+
func createGrantResourceQuery(d *schema.ResourceData, privileges []string, resources []string) string {
292+
var query string
293+
294+
var schemaName = pq.QuoteIdentifier(d.Get("schema").(string))
295+
var quotedResources []string
296+
for _, object := range resources {
297+
quotedResources = append(quotedResources, schemaName+"."+pq.QuoteIdentifier(object))
298+
}
299+
300+
query = fmt.Sprintf(
301+
"GRANT %s ON %s %s TO %s",
302+
strings.Join(privileges, ","),
303+
strings.ToUpper(d.Get("object_type").(string)),
304+
strings.Join(quotedResources, ","),
305+
pq.QuoteIdentifier(d.Get("role").(string)),
306+
)
307+
308+
if d.Get("with_grant_option").(bool) == true {
309+
query = query + " WITH GRANT OPTION"
310+
}
311+
312+
return query
313+
}
314+
315+
func createRevokeResourceQuery(d *schema.ResourceData, resources []string) string {
316+
var query string
317+
318+
var schemaName = pq.QuoteIdentifier(d.Get("schema").(string))
319+
var quotedResources []string
320+
for _, object := range resources {
321+
quotedResources = append(quotedResources, schemaName+"."+pq.QuoteIdentifier(object))
322+
}
323+
324+
query = fmt.Sprintf(
325+
"REVOKE ALL PRIVILEGES ON %s %s FROM %s",
326+
strings.ToUpper(d.Get("object_type").(string)),
327+
strings.Join(quotedResources, ","),
328+
pq.QuoteIdentifier(d.Get("role").(string)),
329+
)
330+
331+
return query
332+
}
333+
334+
func grantRoleResourcePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
335+
var privileges []string
336+
for _, priv := range d.Get("privileges").(*schema.Set).List() {
337+
privileges = append(privileges, priv.(string))
338+
}
339+
340+
if len(privileges) == 0 {
341+
log.Printf("[DEBUG] no privileges to grant for role %s in database: %s,", d.Get("role").(string), d.Get("database"))
342+
return nil
343+
}
344+
345+
var resources []string
346+
for _, priv := range d.Get("resources").(*schema.Set).List() {
347+
resources = append(resources, priv.(string))
348+
}
349+
350+
query := createGrantResourceQuery(d, privileges, resources)
351+
352+
_, err := txn.Exec(query)
353+
return err
354+
}
355+
356+
func revokeRoleResourcePrivileges(txn *sql.Tx, d *schema.ResourceData) error {
357+
var resources []string = nil
358+
for _, priv := range d.Get("resources").(*schema.Set).List() {
359+
resources = append(resources, priv.(string))
360+
}
361+
362+
query := createRevokeResourceQuery(d, resources)
363+
if _, err := txn.Exec(query); err != nil {
364+
return fmt.Errorf("could not execute revoke query: %w", err)
365+
}
366+
return nil
367+
}

0 commit comments

Comments
 (0)