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

Commit 7dccb18

Browse files
committed
New resource: posgresql_default_privileges
This resource allow to manage default privileges for tables or sequences for a specified role in a schema. # Conflicts: # postgresql/resource_postgresql_grant.go # Conflicts: # CHANGELOG.md
1 parent d9f18f3 commit 7dccb18

9 files changed

+416
-54
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ FEATURES:
44

55
* New resource: postgresql_grant. This resource allows to grant privileges on all existing tables or sequences for a specified role in a specified schema.
66
([#53](https://github.com/terraform-providers/terraform-provider-postgresql/pull/53))
7+
* New resource: postgresql_default_privileges. This resource allow to manage default privileges for tables or sequences for a specified role in a specified schema.
8+
([#53](https://github.com/terraform-providers/terraform-provider-postgresql/pull/53))
79

810
## 0.2.1 (February 28, 2019)
911

postgresql/provider.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ func Provider() terraform.ResourceProvider {
100100
},
101101

102102
ResourcesMap: map[string]*schema.Resource{
103-
"postgresql_database": resourcePostgreSQLDatabase(),
104-
"postgresql_extension": resourcePostgreSQLExtension(),
105-
"postgresql_schema": resourcePostgreSQLSchema(),
106-
"postgresql_role": resourcePostgreSQLRole(),
107-
"postgresql_grant": resourcePostgreSQLGrant(),
103+
"postgresql_database": resourcePostgreSQLDatabase(),
104+
"postgresql_default_privileges": resourcePostgreSQLDefaultPrivileges(),
105+
"postgresql_extension": resourcePostgreSQLExtension(),
106+
"postgresql_grant": resourcePostgreSQLGrant(),
107+
"postgresql_schema": resourcePostgreSQLSchema(),
108+
"postgresql_role": resourcePostgreSQLRole(),
108109
},
109110

110111
ConfigureFunc: providerConfigure,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package postgresql
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"log"
7+
"strings"
8+
9+
"github.com/hashicorp/errwrap"
10+
"github.com/hashicorp/terraform/helper/schema"
11+
"github.com/hashicorp/terraform/helper/validation"
12+
13+
// Use Postgres as SQL driver
14+
"github.com/lib/pq"
15+
)
16+
17+
func resourcePostgreSQLDefaultPrivileges() *schema.Resource {
18+
return &schema.Resource{
19+
Create: resourcePostgreSQLDefaultPrivilegesCreate,
20+
Update: resourcePostgreSQLDefaultPrivilegesCreate,
21+
Read: resourcePostgreSQLDefaultPrivilegesRead,
22+
Delete: resourcePostgreSQLDefaultPrivilegesDelete,
23+
24+
Schema: map[string]*schema.Schema{
25+
"role": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
ForceNew: true,
29+
Description: "The name of the role to which grant default privileges on",
30+
},
31+
"database": {
32+
Type: schema.TypeString,
33+
Required: true,
34+
ForceNew: true,
35+
Description: "The database to grant default privileges for this role",
36+
},
37+
"owner": {
38+
Type: schema.TypeString,
39+
Required: true,
40+
ForceNew: true,
41+
Description: "Role for which apply default privileges (You can change default privileges only for objects that will be created by yourself or by roles that you are a member of)",
42+
},
43+
"schema": {
44+
Type: schema.TypeString,
45+
Required: true,
46+
ForceNew: true,
47+
Description: "The database schema to set default privileges for this role",
48+
},
49+
"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 set the default privileges on (one of: table, sequence)",
58+
},
59+
"privileges": &schema.Schema{
60+
Type: schema.TypeSet,
61+
Required: true,
62+
Elem: &schema.Schema{Type: schema.TypeString},
63+
Set: schema.HashString,
64+
MinItems: 1,
65+
Description: "The list of privileges to apply as default privileges",
66+
},
67+
},
68+
}
69+
}
70+
71+
func resourcePostgreSQLDefaultPrivilegesRead(d *schema.ResourceData, meta interface{}) error {
72+
client := meta.(*Client)
73+
74+
client.catalogLock.RLock()
75+
defer client.catalogLock.RUnlock()
76+
77+
exists, err := checkRoleDBSchemaExists(client, d)
78+
if err != nil {
79+
return err
80+
}
81+
if !exists {
82+
d.SetId("")
83+
return nil
84+
}
85+
86+
txn, err := startTransaction(client, d.Get("database").(string))
87+
if err != nil {
88+
return err
89+
}
90+
defer deferredRollback(txn)
91+
92+
return readRoleDefaultPrivileges(txn, d)
93+
}
94+
95+
func resourcePostgreSQLDefaultPrivilegesCreate(d *schema.ResourceData, meta interface{}) error {
96+
if err := validatePrivileges(d.Get("object_type").(string), d.Get("privileges").(*schema.Set).List()); err != nil {
97+
return err
98+
}
99+
100+
database := d.Get("database").(string)
101+
102+
client := meta.(*Client)
103+
104+
client.catalogLock.Lock()
105+
defer client.catalogLock.Unlock()
106+
107+
txn, err := startTransaction(client, database)
108+
if err != nil {
109+
return err
110+
}
111+
defer deferredRollback(txn)
112+
113+
// Revoke all privileges before granting otherwise reducing privileges will not work.
114+
// We just have to revoke them in the same transaction so role will not lost his privileges between revoke and grant.
115+
if err = revokeRoleDefaultPrivileges(txn, d); err != nil {
116+
return err
117+
}
118+
119+
if err = grantRoleDefaultPrivileges(txn, d); err != nil {
120+
return err
121+
}
122+
123+
if err := txn.Commit(); err != nil {
124+
return err
125+
}
126+
127+
d.SetId(generateDefaultPrivilegesID(d))
128+
129+
txn, err = startTransaction(client, d.Get("database").(string))
130+
if err != nil {
131+
return err
132+
}
133+
defer deferredRollback(txn)
134+
135+
return readRoleDefaultPrivileges(txn, d)
136+
}
137+
138+
func resourcePostgreSQLDefaultPrivilegesDelete(d *schema.ResourceData, meta interface{}) error {
139+
client := meta.(*Client)
140+
141+
client.catalogLock.Lock()
142+
defer client.catalogLock.Unlock()
143+
144+
txn, err := startTransaction(client, d.Get("database").(string))
145+
if err != nil {
146+
return err
147+
}
148+
defer deferredRollback(txn)
149+
150+
revokeRoleDefaultPrivileges(txn, d)
151+
if err := txn.Commit(); err != nil {
152+
return err
153+
}
154+
155+
return nil
156+
}
157+
158+
func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error {
159+
role := d.Get("role").(string)
160+
owner := d.Get("owner").(string)
161+
pgSchema := d.Get("schema").(string)
162+
objectType := d.Get("object_type").(string)
163+
164+
// This query aggregates the list of default privileges type (prtype)
165+
// for the role (grantee), owner (grantor), schema (namespace name)
166+
// and the specified object type (defaclobjtype).
167+
query := `SELECT array_agg(prtype) FROM (
168+
SELECT defaclnamespace, (aclexplode(defaclacl)).* FROM pg_default_acl
169+
WHERE defaclobjtype = $3
170+
) AS t (namespace, grantor_oid, grantee_oid, prtype, grantable)
171+
172+
JOIN pg_namespace ON pg_namespace.oid = namespace
173+
WHERE pg_get_userbyid(grantee_oid) = $1 AND nspname = $2 AND pg_get_userbyid(grantor_oid) = $4;
174+
`
175+
var privileges pq.ByteaArray
176+
177+
if err := txn.QueryRow(
178+
query, role, pgSchema, objectTypes[objectType], owner,
179+
).Scan(&privileges); err != nil {
180+
return errwrap.Wrapf("could not read default privileges: {{err}}", err)
181+
}
182+
183+
// We consider no privileges as "not exists"
184+
if len(privileges) == 0 {
185+
log.Printf("[DEBUG] no default privileges for role %s in schema %s", role, pgSchema)
186+
d.SetId("")
187+
return nil
188+
}
189+
190+
privilegesSet := pgArrayToSet(privileges)
191+
d.Set("privileges", privilegesSet)
192+
d.SetId(generateDefaultPrivilegesID(d))
193+
194+
return nil
195+
}
196+
197+
func grantRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error {
198+
role := d.Get("role").(string)
199+
pgSchema := d.Get("schema").(string)
200+
201+
privileges := []string{}
202+
for _, priv := range d.Get("privileges").(*schema.Set).List() {
203+
privileges = append(privileges, priv.(string))
204+
}
205+
206+
// TODO: We grant default privileges for the DB owner
207+
// For that we need to be either superuser or a member of the owner role.
208+
// With AWS RDS, It's not possible to create superusers as it is restricted by AWS itself.
209+
// In that case, the only solution would be to have the PostgreSQL user used by Terraform
210+
// to be also part of the database owner role.
211+
212+
query := fmt.Sprintf("ALTER DEFAULT PRIVILEGES FOR ROLE %s IN SCHEMA %s GRANT %s ON %sS TO %s",
213+
pq.QuoteIdentifier(d.Get("owner").(string)),
214+
pq.QuoteIdentifier(pgSchema),
215+
strings.Join(privileges, ","),
216+
strings.ToUpper(d.Get("object_type").(string)),
217+
pq.QuoteIdentifier(role),
218+
)
219+
220+
_, err := txn.Exec(
221+
query,
222+
)
223+
if err != nil {
224+
return errwrap.Wrapf("could not alter default privileges: {{err}}", err)
225+
}
226+
227+
return nil
228+
}
229+
230+
func revokeRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error {
231+
query := fmt.Sprintf(
232+
"ALTER DEFAULT PRIVILEGES FOR ROLE %s IN SCHEMA %s REVOKE ALL ON %sS FROM %s",
233+
pq.QuoteIdentifier(d.Get("owner").(string)),
234+
pq.QuoteIdentifier(d.Get("schema").(string)),
235+
strings.ToUpper(d.Get("object_type").(string)),
236+
pq.QuoteIdentifier(d.Get("role").(string)),
237+
)
238+
239+
_, err := txn.Exec(query)
240+
return err
241+
}
242+
243+
func generateDefaultPrivilegesID(d *schema.ResourceData) string {
244+
return strings.Join([]string{
245+
d.Get("role").(string), d.Get("database").(string), d.Get("schema").(string),
246+
d.Get("owner").(string), d.Get("object_type").(string),
247+
}, "_")
248+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package postgresql
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform/helper/resource"
8+
"github.com/hashicorp/terraform/terraform"
9+
)
10+
11+
func TestAccPostgresqlDefaultPrivileges(t *testing.T) {
12+
// We have to create the database outside of resource.Test
13+
// because we need to create a table to assert that grant are correctly applied
14+
// and we don't have this resource yet
15+
dbSuffix, teardown := setupTestDatabase(t, true, true)
16+
defer teardown()
17+
18+
config := getTestConfig(t)
19+
dbName, roleName := getTestDBNames(dbSuffix)
20+
21+
// We set PGUSER as owner as he will create the test table
22+
var testDPSelect = fmt.Sprintf(`
23+
resource "postgresql_default_privileges" "test_ro" {
24+
database = "%s"
25+
owner = "%s"
26+
role = "%s"
27+
schema = "public"
28+
object_type = "table"
29+
privileges = ["SELECT"]
30+
}
31+
`, dbName, config.Username, roleName)
32+
33+
resource.Test(t, resource.TestCase{
34+
PreCheck: func() {
35+
testAccPreCheck(t)
36+
testCheckCompatibleVersion(t, featurePrivileges)
37+
},
38+
Providers: testAccProviders,
39+
Steps: []resource.TestStep{
40+
{
41+
Config: testDPSelect,
42+
Check: resource.ComposeTestCheckFunc(
43+
func(*terraform.State) error {
44+
tables := []string{"test_table"}
45+
// To test default privileges, we need to create a table
46+
// after having apply the state.
47+
dropFunc := createTestTables(t, dbSuffix, tables)
48+
defer dropFunc()
49+
50+
return testCheckTablesPrivileges(t, dbSuffix, tables, []string{"SELECT"})
51+
},
52+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "object_type", "table"),
53+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "privileges.#", "1"),
54+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "privileges.3138006342", "SELECT"),
55+
),
56+
},
57+
},
58+
})
59+
}

postgresql/resource_postgresql_grant_test.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010

1111
func TestAccPostgresqlGrant(t *testing.T) {
1212
// We have to create the database outside of resource.Test
13-
// because we need to create a table to assert that grant are correctly applied
13+
// because we need to create tables to assert that grant are correctly applied
1414
// and we don't have this resource yet
15-
dbSuffix, teardown := setupTestDatabase(t, true, true, true)
15+
dbSuffix, teardown := setupTestDatabase(t, true, true)
1616
defer teardown()
1717

18+
testTables := []string{"test_table", "test_table2"}
19+
createTestTables(t, dbSuffix, testTables)
20+
1821
dbName, roleName := getTestDBNames(dbSuffix)
1922
var testGrantSelect = fmt.Sprintf(`
2023
resource "postgresql_grant" "test_ro" {
@@ -46,23 +49,23 @@ func TestAccPostgresqlGrant(t *testing.T) {
4649
{
4750
Config: testGrantSelect,
4851
Check: resource.ComposeTestCheckFunc(
49-
func(*terraform.State) error {
50-
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT"}, false)
51-
},
5252
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.#", "1"),
5353
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.3138006342", "SELECT"),
54+
func(*terraform.State) error {
55+
return testCheckTablesPrivileges(t, dbSuffix, testTables, []string{"SELECT"})
56+
},
5457
),
5558
},
5659
{
5760
Config: testGrantSelectInsertUpdate,
5861
Check: resource.ComposeTestCheckFunc(
59-
func(*terraform.State) error {
60-
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT", "INSERT", "UPDATE"}, false)
61-
},
6262
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.#", "3"),
6363
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.3138006342", "SELECT"),
6464
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.892623219", "INSERT"),
6565
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.1759376126", "UPDATE"),
66+
func(*terraform.State) error {
67+
return testCheckTablesPrivileges(t, dbSuffix, testTables, []string{"SELECT", "INSERT", "UPDATE"})
68+
},
6669
),
6770
},
6871
},

0 commit comments

Comments
 (0)