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

Commit 80255dd

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.
1 parent 16b4d39 commit 80255dd

6 files changed

+334
-28
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
([#51](https://github.com/terraform-providers/terraform-provider-postgresql/pull/51))
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

911
## 0.2.1 (Unreleased)

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,75 @@
1+
package postgresql
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform/helper/resource"
9+
"github.com/hashicorp/terraform/terraform"
10+
)
11+
12+
func TestAccPostgresqlDefaultPrivileges(t *testing.T) {
13+
// We have to create the database outside of resource.Test
14+
// because we need to create a table to assert that grant are correctly applied
15+
// and we don't have this resource yet
16+
dbSuffix, teardown := setupTestDatabase(t, true, true, false)
17+
defer teardown()
18+
19+
config := getTestConfig(t)
20+
dbName, roleName := getTestDBNames(dbSuffix)
21+
22+
// We set PGUSER as owner as he will create the test table
23+
var testDPSelect = fmt.Sprintf(`
24+
resource "postgresql_default_privileges" "test_ro" {
25+
database = "%s"
26+
owner = "%s"
27+
role = "%s"
28+
schema = "public"
29+
object_type = "table"
30+
privileges = ["SELECT"]
31+
}
32+
`, dbName, config.Username, roleName)
33+
34+
resource.Test(t, resource.TestCase{
35+
PreCheck: func() { testAccPreCheck(t) },
36+
Providers: testAccProviders,
37+
Steps: []resource.TestStep{
38+
{
39+
Config: testDPSelect,
40+
Check: resource.ComposeTestCheckFunc(
41+
func(*terraform.State) error {
42+
// To test default privileges, we need to create a table
43+
// after having apply the state.
44+
dropFunc := createTestTable(t, dbSuffix)
45+
defer dropFunc()
46+
47+
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT"})
48+
},
49+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "object_type", "table"),
50+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "privileges.#", "1"),
51+
resource.TestCheckResourceAttr("postgresql_default_privileges.test_ro", "privileges.3138006342", "SELECT"),
52+
),
53+
},
54+
},
55+
})
56+
}
57+
58+
func createTestTable(t *testing.T, dbSuffix string) func() {
59+
config := getTestConfig(t)
60+
dbName, _ := getTestDBNames(dbSuffix)
61+
62+
db, err := sql.Open("postgres", config.connStr(dbName))
63+
if err != nil {
64+
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
65+
}
66+
defer db.Close()
67+
68+
if _, err := db.Exec(testTableDef); err != nil {
69+
t.Fatalf("could not create test table in db %s: %v", dbName, err)
70+
}
71+
// In this case we need to drop table after each test.
72+
return func() {
73+
db.Exec("DROP TABLE test_table")
74+
}
75+
}

postgresql/resource_postgresql_grant_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestAccPostgresqlGrant(t *testing.T) {
4444
Config: testGrantSelect,
4545
Check: resource.ComposeTestCheckFunc(
4646
func(*terraform.State) error {
47-
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT"}, false)
47+
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT"})
4848
},
4949
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.#", "1"),
5050
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.3138006342", "SELECT"),
@@ -54,7 +54,7 @@ func TestAccPostgresqlGrant(t *testing.T) {
5454
Config: testGrantSelectInsertUpdate,
5555
Check: resource.ComposeTestCheckFunc(
5656
func(*terraform.State) error {
57-
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT", "INSERT", "UPDATE"}, false)
57+
return testCheckTablePrivileges(t, dbSuffix, []string{"SELECT", "INSERT", "UPDATE"})
5858
},
5959
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.#", "3"),
6060
resource.TestCheckResourceAttr("postgresql_grant.test_ro", "privileges.3138006342", "SELECT"),

0 commit comments

Comments
 (0)