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

Commit

Permalink
Create new resource: postgresql_grant
Browse files Browse the repository at this point in the history
This resource allow to grant privileges on all existing tables or sequences
for a specified role in a specified schema.
  • Loading branch information
cyrilgdn committed Feb 20, 2019
1 parent e5b6c42 commit 12a4bda
Show file tree
Hide file tree
Showing 15 changed files with 825 additions and 25 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
## 0.3.0 (Unreleased)

FEATURES:

* New resource: postgresql_grant. This resource allows to grant privileges on all existing tables or sequences for a specified role in a specified schema.
([#51](https://github.com/terraform-providers/terraform-provider-postgresql/pull/51))


## 0.2.0 (Unreleased)
FEATURES:

* Add `database_username` in provider configuration to manage [user name maps](https://www.postgresql.org/docs/current/auth-username-maps.html)
([#58](https://github.com/terraform-providers/terraform-provider-postgresql/pull/58))

BUG FIXES:

* `create_database` is now being applied correctly on role creation
Expand Down
28 changes: 16 additions & 12 deletions postgresql/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ var (
type Config struct {
Host string
Port int
Database string
Username string
Password string
DatabaseUsername string
Expand All @@ -89,6 +88,8 @@ type Client struct {
// Configuration for the client
config Config

databaseName string

// db is a pointer to the DB connection. Callers are responsible for
// releasing their connections.
db *sql.DB
Expand All @@ -105,21 +106,23 @@ type Client struct {
catalogLock sync.RWMutex
}

// NewClient returns new client config
func (c *Config) NewClient() (*Client, error) {
// NewClient returns client config for the specified database.
func (c *Config) NewClient(database string) (*Client, error) {
dbRegistryLock.Lock()
defer dbRegistryLock.Unlock()

dsn := c.connStr()
dsn := c.connStr(database)
dbEntry, found := dbRegistry[dsn]
if !found {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, errwrap.Wrapf("Error connecting to PostgreSQL server: {{err}}", err)
}

// only one connection
db.SetMaxIdleConns(1)
// We don't want to retain connection
// So when we connect on a specific database which might be managed by terraform,
// we don't keep opened connection in case of the db has to be dopped in the plan.
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(c.MaxConns)

version, err := fingerprintCapabilities(db)
Expand All @@ -136,9 +139,10 @@ func (c *Config) NewClient() (*Client, error) {
}

client := Client{
config: *c,
db: dbEntry.db,
version: dbEntry.version,
config: *c,
databaseName: database,
db: dbEntry.db,
version: dbEntry.version,
}

return &client, nil
Expand All @@ -157,7 +161,7 @@ func (c *Config) featureSupported(name featureName) bool {
return fn(c.ExpectedVersion)
}

func (c *Config) connStr() string {
func (c *Config) connStr(database string) string {
// NOTE: dbname must come before user otherwise dbname will be set to
// user.
var dsnFmt string
Expand Down Expand Up @@ -212,7 +216,7 @@ func (c *Config) connStr() string {
logValues := []interface{}{
quote(c.Host),
c.Port,
quote(c.Database),
quote(database),
quote(c.Username),
quote("<redacted>"),
quote(c.SSLMode),
Expand All @@ -231,7 +235,7 @@ func (c *Config) connStr() string {
connValues := []interface{}{
quote(c.Host),
c.Port,
quote(c.Database),
quote(database),
quote(c.Username),
quote(c.Password),
quote(c.SSLMode),
Expand Down
96 changes: 96 additions & 0 deletions postgresql/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"database/sql"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)

Expand Down Expand Up @@ -91,3 +92,98 @@ func revokeRoleMembership(db *sql.DB, role, member string) error {
}
return nil
}

func sliceContainsStr(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}

// allowedPrivileges is the list of privileges allowed per object types in Postgres.
// see: https://www.postgresql.org/docs/current/sql-grant.html
var allowedPrivileges = map[string][]string{
"table": []string{"ALL", "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"},
"sequence": []string{"ALL", "USAGE", "SELECT", "UPDATE"},
}

// validatePrivileges checks that privileges to apply are allowed for this object type.
func validatePrivileges(objectType string, privileges []interface{}) error {
allowed, ok := allowedPrivileges[objectType]
if !ok {
return fmt.Errorf("unknown object type %s", objectType)
}

for _, priv := range privileges {
if !sliceContainsStr(allowed, priv.(string)) {
return fmt.Errorf("%s is not an allowed privilege for object type %s", priv, objectType)
}
}
return nil
}

func pgArrayToSet(arr pq.ByteaArray) *schema.Set {
s := make([]interface{}, len(arr))
for i, v := range arr {
s[i] = string(v)
}
return schema.NewSet(schema.HashString, s)
}

// startTransaction starts a new DB transaction on the specified database.
// If the database is specified and different from the one configured in the provider,
// it will create a new connection pool if needed.
func startTransaction(client *Client, database string) (*sql.Tx, error) {
if database != "" && database != client.databaseName {
var err error
client, err = client.config.NewClient(database)
if err != nil {
return nil, err
}
}
db := client.DB()
txn, err := db.Begin()
if err != nil {
return nil, errwrap.Wrapf("could not start transaction: {{err}}", err)
}

return txn, nil
}

func dbExists(txn *sql.Tx, dbname string) (bool, error) {
err := txn.QueryRow("SELECT datname FROM pg_database WHERE datname=$1", dbname).Scan(&dbname)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, errwrap.Wrapf("could not check if database exists: {{err}}", err)
}

return true, nil
}

func roleExists(txn *sql.Tx, rolname string) (bool, error) {
err := txn.QueryRow("SELECT 1 FROM pg_roles WHERE rolname=$1", rolname).Scan(&rolname)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, errwrap.Wrapf("could not check if role exists: {{err}}", err)
}

return true, nil
}

func schemaExists(txn *sql.Tx, schemaname string) (bool, error) {
err := txn.QueryRow("SELECT 1 FROM pg_namespace WHERE nspname=$1", schemaname).Scan(&schemaname)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, errwrap.Wrapf("could not check if schema exists: {{err}}", err)
}

return true, nil
}
4 changes: 2 additions & 2 deletions postgresql/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func Provider() terraform.ResourceProvider {
"postgresql_extension": resourcePostgreSQLExtension(),
"postgresql_schema": resourcePostgreSQLSchema(),
"postgresql_role": resourcePostgreSQLRole(),
"postgresql_grant": resourcePostgreSQLGrant(),
},

ConfigureFunc: providerConfigure,
Expand Down Expand Up @@ -141,7 +142,6 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
Host: d.Get("host").(string),
Port: d.Get("port").(int),
Database: d.Get("database").(string),
Username: d.Get("username").(string),
Password: d.Get("password").(string),
DatabaseUsername: d.Get("database_username").(string),
Expand All @@ -152,7 +152,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
ExpectedVersion: version,
}

client, err := config.NewClient()
client, err := config.NewClient(d.Get("database").(string))
if err != nil {
return nil, errwrap.Wrapf("Error initializing PostgreSQL client: {{err}}", err)
}
Expand Down
11 changes: 4 additions & 7 deletions postgresql/resource_postgresql_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,16 +279,13 @@ func resourcePostgreSQLDatabaseExists(d *schema.ResourceData, meta interface{})
c.catalogLock.RLock()
defer c.catalogLock.RUnlock()

var dbName string
err := c.DB().QueryRow("SELECT d.datname from pg_database d WHERE datname=$1", d.Id()).Scan(&dbName)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
txn, err := startTransaction(c, "")
if err != nil {
return false, err
}
defer txn.Rollback()

return true, nil
return dbExists(txn, d.Id())
}

func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error {
Expand Down
4 changes: 2 additions & 2 deletions postgresql/resource_postgresql_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func TestAccPostgresqlDatabase_GrantOwner(t *testing.T) {
skipIfNotAcc(t)

config := getTestConfig(t)
dsn := config.connStr()
dsn := config.connStr("postgres")

var stateConfig = `
resource postgresql_role "test_owner" {
Expand Down Expand Up @@ -226,7 +226,7 @@ func TestAccPostgresqlDatabase_GrantOwnerNotNeeded(t *testing.T) {
skipIfNotAcc(t)

config := getTestConfig(t)
dsn := config.connStr()
dsn := config.connStr("postgres")

dbExecute(
t, dsn,
Expand Down
Loading

0 comments on commit 12a4bda

Please sign in to comment.