2016-12-12 16:35:41 +01:00
package postgresql
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
2016-12-25 13:18:32 +01:00
"reflect"
"strings"
2016-12-12 16:35:41 +01:00
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
2016-12-25 13:18:32 +01:00
"github.com/sean-/postgresql-acl"
2016-12-12 16:35:41 +01:00
)
const (
2016-12-25 13:18:32 +01:00
schemaNameAttr = "name"
schemaOwnerAttr = "owner"
schemaPolicyAttr = "policy"
schemaIfNotExists = "if_not_exists"
schemaPolicyCreateAttr = "create"
schemaPolicyCreateWithGrantAttr = "create_with_grant"
schemaPolicyRoleAttr = "role"
schemaPolicyUsageAttr = "usage"
schemaPolicyUsageWithGrantAttr = "usage_with_grant"
2016-12-12 16:35:41 +01:00
)
func resourcePostgreSQLSchema ( ) * schema . Resource {
return & schema . Resource {
Create : resourcePostgreSQLSchemaCreate ,
Read : resourcePostgreSQLSchemaRead ,
Update : resourcePostgreSQLSchemaUpdate ,
Delete : resourcePostgreSQLSchemaDelete ,
Importer : & schema . ResourceImporter {
State : schema . ImportStatePassthrough ,
} ,
Schema : map [ string ] * schema . Schema {
schemaNameAttr : {
Type : schema . TypeString ,
Required : true ,
Description : "The name of the schema" ,
} ,
2016-12-14 10:04:35 +01:00
schemaOwnerAttr : {
Type : schema . TypeString ,
Optional : true ,
Computed : true ,
Description : "The ROLE name who owns the schema" ,
} ,
2016-12-25 13:18:32 +01:00
schemaIfNotExists : {
Type : schema . TypeBool ,
Optional : true ,
Default : true ,
Description : "When true, use the existing schema if it exsts" ,
} ,
schemaPolicyAttr : & schema . Schema {
Type : schema . TypeSet ,
Optional : true ,
Computed : true ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
schemaPolicyCreateAttr : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "If true, allow the specified ROLEs to CREATE new objects within the schema(s)" ,
ConflictsWith : [ ] string { schemaPolicyAttr + "." + schemaPolicyCreateWithGrantAttr } ,
} ,
schemaPolicyCreateWithGrantAttr : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "If true, allow the specified ROLEs to CREATE new objects within the schema(s) and GRANT the same CREATE privilege to different ROLEs" ,
ConflictsWith : [ ] string { schemaPolicyAttr + "." + schemaPolicyCreateAttr } ,
} ,
schemaPolicyRoleAttr : {
Type : schema . TypeString ,
Elem : & schema . Schema { Type : schema . TypeString } ,
Required : true ,
Description : "ROLE who will receive this policy" ,
} ,
schemaPolicyUsageAttr : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "If true, allow the specified ROLEs to use objects within the schema(s)" ,
ConflictsWith : [ ] string { schemaPolicyAttr + "." + schemaPolicyUsageWithGrantAttr } ,
} ,
schemaPolicyUsageWithGrantAttr : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "If true, allow the specified ROLEs to use objects within the schema(s) and GRANT the same USAGE privilege to different ROLEs" ,
ConflictsWith : [ ] string { schemaPolicyAttr + "." + schemaPolicyUsageAttr } ,
} ,
} ,
} ,
} ,
2016-12-12 16:35:41 +01:00
} ,
}
}
func resourcePostgreSQLSchemaCreate ( d * schema . ResourceData , meta interface { } ) error {
2016-12-25 13:18:32 +01:00
queries := [ ] string { }
schemaName := d . Get ( schemaNameAttr ) . ( string )
{
b := bytes . NewBufferString ( "CREATE SCHEMA " )
if v := d . Get ( schemaIfNotExists ) ; v . ( bool ) {
fmt . Fprint ( b , "IF NOT EXISTS " )
}
fmt . Fprint ( b , pq . QuoteIdentifier ( schemaName ) )
switch v , ok := d . GetOk ( schemaOwnerAttr ) ; {
case ok :
fmt . Fprint ( b , " AUTHORIZATION " , pq . QuoteIdentifier ( v . ( string ) ) )
}
queries = append ( queries , b . String ( ) )
}
// ACL objects that can generate the necessary SQL
type RoleKey string
var schemaPolicies map [ RoleKey ] acl . Schema
if policiesRaw , ok := d . GetOk ( schemaPolicyAttr ) ; ok {
policiesList := policiesRaw . ( * schema . Set ) . List ( )
// NOTE: len(policiesList) doesn't take into account multiple
// roles per policy.
schemaPolicies = make ( map [ RoleKey ] acl . Schema , len ( policiesList ) )
for _ , policyRaw := range policiesList {
policyMap := policyRaw . ( map [ string ] interface { } )
rolePolicy := schemaPolicyToACL ( policyMap )
roleKey := RoleKey ( strings . ToLower ( rolePolicy . Role ) )
if existingRolePolicy , ok := schemaPolicies [ roleKey ] ; ok {
schemaPolicies [ roleKey ] = existingRolePolicy . Merge ( rolePolicy )
} else {
schemaPolicies [ roleKey ] = rolePolicy
}
}
}
for _ , policy := range schemaPolicies {
queries = append ( queries , policy . Grants ( schemaName ) ... )
}
2016-12-12 16:35:41 +01:00
c := meta . ( * Client )
conn , err := c . Connect ( )
if err != nil {
return errwrap . Wrapf ( "Error connecting to PostgreSQL: {{err}}" , err )
}
defer conn . Close ( )
2016-12-25 13:18:32 +01:00
txn , err := conn . Begin ( )
if err != nil {
return err
}
defer txn . Rollback ( )
2016-12-12 16:35:41 +01:00
2016-12-25 13:18:32 +01:00
for _ , query := range queries {
_ , err = txn . Query ( query )
if err != nil {
return errwrap . Wrapf ( fmt . Sprintf ( "Error creating schema %s: {{err}}" , schemaName ) , err )
}
2016-12-14 10:04:35 +01:00
}
2016-12-25 13:18:32 +01:00
if err := txn . Commit ( ) ; err != nil {
return errwrap . Wrapf ( "Error committing schema: {{err}}" , err )
2016-12-12 16:35:41 +01:00
}
d . SetId ( schemaName )
return resourcePostgreSQLSchemaRead ( d , meta )
}
func resourcePostgreSQLSchemaDelete ( d * schema . ResourceData , meta interface { } ) error {
client := meta . ( * Client )
conn , err := client . Connect ( )
if err != nil {
return err
}
defer conn . Close ( )
2016-12-25 13:18:32 +01:00
txn , err := conn . Begin ( )
if err != nil {
return err
}
defer txn . Rollback ( )
2016-12-12 16:35:41 +01:00
schemaName := d . Get ( schemaNameAttr ) . ( string )
2016-12-25 13:18:32 +01:00
// NOTE(sean@): Deliberately not performing a cascading drop.
2016-12-12 16:35:41 +01:00
query := fmt . Sprintf ( "DROP SCHEMA %s" , pq . QuoteIdentifier ( schemaName ) )
2016-12-25 13:18:32 +01:00
_ , err = txn . Query ( query )
2016-12-12 16:35:41 +01:00
if err != nil {
return errwrap . Wrapf ( "Error deleting schema: {{err}}" , err )
}
2016-12-25 13:18:32 +01:00
if err := txn . Commit ( ) ; err != nil {
return errwrap . Wrapf ( "Error committing schema: {{err}}" , err )
}
2016-12-12 16:35:41 +01:00
d . SetId ( "" )
return nil
}
func resourcePostgreSQLSchemaRead ( d * schema . ResourceData , meta interface { } ) error {
c := meta . ( * Client )
conn , err := c . Connect ( )
if err != nil {
return err
}
defer conn . Close ( )
schemaId := d . Id ( )
2016-12-14 10:04:35 +01:00
var schemaName , schemaOwner string
2016-12-25 13:18:32 +01:00
var schemaACLs [ ] string
err = conn . QueryRow ( "SELECT n.nspname, pg_catalog.pg_get_userbyid(n.nspowner), COALESCE(n.nspacl, '{}'::aclitem[])::TEXT[] FROM pg_catalog.pg_namespace n WHERE n.nspname=$1" , schemaId ) . Scan ( & schemaName , & schemaOwner , pq . Array ( & schemaACLs ) )
2016-12-12 16:35:41 +01:00
switch {
case err == sql . ErrNoRows :
log . Printf ( "[WARN] PostgreSQL schema (%s) not found" , schemaId )
d . SetId ( "" )
return nil
case err != nil :
return errwrap . Wrapf ( "Error reading schema: {{err}}" , err )
default :
2016-12-25 13:18:32 +01:00
type RoleKey string
schemaPolicies := make ( map [ RoleKey ] acl . Schema , len ( schemaACLs ) )
for _ , aclStr := range schemaACLs {
aclItem , err := acl . Parse ( aclStr )
if err != nil {
return errwrap . Wrapf ( "Error parsing aclitem: {{err}}" , err )
}
schemaACL , err := acl . NewSchema ( aclItem )
if err != nil {
return errwrap . Wrapf ( "invalid perms for schema: {{err}}" , err )
}
roleKey := RoleKey ( strings . ToLower ( schemaACL . Role ) )
var mergedPolicy acl . Schema
if existingRolePolicy , ok := schemaPolicies [ roleKey ] ; ok {
mergedPolicy = existingRolePolicy . Merge ( schemaACL )
} else {
mergedPolicy = schemaACL
}
schemaPolicies [ roleKey ] = mergedPolicy
}
2016-12-12 16:35:41 +01:00
d . Set ( schemaNameAttr , schemaName )
2016-12-14 10:04:35 +01:00
d . Set ( schemaOwnerAttr , schemaOwner )
2016-12-12 16:35:41 +01:00
d . SetId ( schemaName )
return nil
}
}
func resourcePostgreSQLSchemaUpdate ( d * schema . ResourceData , meta interface { } ) error {
c := meta . ( * Client )
conn , err := c . Connect ( )
if err != nil {
return err
}
defer conn . Close ( )
2016-12-25 13:18:32 +01:00
txn , err := conn . Begin ( )
if err != nil {
return err
}
defer txn . Rollback ( )
if err := setSchemaName ( txn , d ) ; err != nil {
return err
}
if err := setSchemaOwner ( txn , d ) ; err != nil {
2016-12-12 16:35:41 +01:00
return err
}
2016-12-25 13:18:32 +01:00
if err := setSchemaPolicy ( txn , d ) ; err != nil {
2016-12-14 10:04:35 +01:00
return err
}
2016-12-25 13:18:32 +01:00
if err := txn . Commit ( ) ; err != nil {
return errwrap . Wrapf ( "Error committing schema: {{err}}" , err )
}
2016-12-12 16:35:41 +01:00
return resourcePostgreSQLSchemaRead ( d , meta )
}
2016-12-25 13:18:32 +01:00
func setSchemaName ( txn * sql . Tx , d * schema . ResourceData ) error {
2016-12-12 16:35:41 +01:00
if ! d . HasChange ( schemaNameAttr ) {
return nil
}
oraw , nraw := d . GetChange ( schemaNameAttr )
o := oraw . ( string )
n := nraw . ( string )
if n == "" {
return errors . New ( "Error setting schema name to an empty string" )
}
query := fmt . Sprintf ( "ALTER SCHEMA %s RENAME TO %s" , pq . QuoteIdentifier ( o ) , pq . QuoteIdentifier ( n ) )
2016-12-25 13:18:32 +01:00
if _ , err := txn . Query ( query ) ; err != nil {
2016-12-12 16:35:41 +01:00
return errwrap . Wrapf ( "Error updating schema NAME: {{err}}" , err )
}
d . SetId ( n )
return nil
}
2016-12-14 10:04:35 +01:00
2016-12-25 13:18:32 +01:00
func setSchemaOwner ( txn * sql . Tx , d * schema . ResourceData ) error {
2016-12-14 10:04:35 +01:00
if ! d . HasChange ( schemaOwnerAttr ) {
return nil
}
oraw , nraw := d . GetChange ( schemaOwnerAttr )
o := oraw . ( string )
n := nraw . ( string )
if n == "" {
return errors . New ( "Error setting schema owner to an empty string" )
}
query := fmt . Sprintf ( "ALTER SCHEMA %s OWNER TO %s" , pq . QuoteIdentifier ( o ) , pq . QuoteIdentifier ( n ) )
2016-12-25 13:18:32 +01:00
if _ , err := txn . Query ( query ) ; err != nil {
2016-12-14 10:04:35 +01:00
return errwrap . Wrapf ( "Error updating schema OWNER: {{err}}" , err )
}
return nil
}
2016-12-25 13:18:32 +01:00
func setSchemaPolicy ( txn * sql . Tx , d * schema . ResourceData ) error {
if ! d . HasChange ( schemaPolicyAttr ) {
return nil
}
schemaName := d . Get ( schemaNameAttr ) . ( string )
oraw , nraw := d . GetChange ( schemaPolicyAttr )
oldList := oraw . ( * schema . Set ) . List ( )
newList := nraw . ( * schema . Set ) . List ( )
queries := make ( [ ] string , 0 , len ( oldList ) + len ( newList ) )
dropped , added , updated , _ := schemaChangedPolicies ( oldList , newList )
for _ , p := range dropped {
pMap := p . ( map [ string ] interface { } )
rolePolicy := schemaPolicyToACL ( pMap )
2016-12-25 13:56:39 +01:00
var foundUser bool
err := txn . QueryRow ( ` SELECT TRUE FROM pg_catalog.pg_user WHERE usename = $1 ` , rolePolicy . Role ) . Scan ( & foundUser )
switch {
case err == sql . ErrNoRows :
// Don't execute this role's REVOKEs because the role
// was dropped first and therefore doesn't exist.
case err != nil :
return errwrap . Wrapf ( "Error reading schema: {{err}}" , err )
default :
queries = append ( queries , rolePolicy . Revokes ( schemaName ) ... )
}
2016-12-25 13:18:32 +01:00
}
for _ , p := range added {
pMap := p . ( map [ string ] interface { } )
rolePolicy := schemaPolicyToACL ( pMap )
queries = append ( queries , rolePolicy . Grants ( schemaName ) ... )
}
for _ , p := range updated {
policies := p . ( [ ] interface { } )
if len ( policies ) != 2 {
panic ( "expected 2 policies, old and new" )
}
{
oldPolicies := policies [ 0 ] . ( map [ string ] interface { } )
rolePolicy := schemaPolicyToACL ( oldPolicies )
queries = append ( queries , rolePolicy . Revokes ( schemaName ) ... )
}
{
newPolicies := policies [ 1 ] . ( map [ string ] interface { } )
rolePolicy := schemaPolicyToACL ( newPolicies )
queries = append ( queries , rolePolicy . Grants ( schemaName ) ... )
}
}
for _ , query := range queries {
if _ , err := txn . Query ( query ) ; err != nil {
return errwrap . Wrapf ( "Error updating schema DCL: {{err}}" , err )
}
}
return nil
}
// schemaChangedPolicies walks old and new to create a set of queries that can
// be executed to enact each type of state change (roles that have been dropped
// from the policy, added to a policy, have updated privilges, or are
// unchanged).
func schemaChangedPolicies ( old , new [ ] interface { } ) ( dropped , added , update , unchanged map [ string ] interface { } ) {
type RoleKey string
oldLookupMap := make ( map [ RoleKey ] interface { } , len ( old ) )
for idx , _ := range old {
v := old [ idx ]
schemaPolicy := v . ( map [ string ] interface { } )
if roleRaw , ok := schemaPolicy [ schemaPolicyRoleAttr ] ; ok {
role := roleRaw . ( string )
roleKey := strings . ToLower ( role )
oldLookupMap [ RoleKey ( roleKey ) ] = schemaPolicy
}
}
newLookupMap := make ( map [ RoleKey ] interface { } , len ( new ) )
for idx , _ := range new {
v := new [ idx ]
schemaPolicy := v . ( map [ string ] interface { } )
if roleRaw , ok := schemaPolicy [ schemaPolicyRoleAttr ] ; ok {
role := roleRaw . ( string )
roleKey := strings . ToLower ( role )
newLookupMap [ RoleKey ( roleKey ) ] = schemaPolicy
}
}
droppedRoles := make ( map [ string ] interface { } , len ( old ) )
for kOld , vOld := range oldLookupMap {
if _ , ok := newLookupMap [ kOld ] ; ! ok {
droppedRoles [ string ( kOld ) ] = vOld
}
}
addedRoles := make ( map [ string ] interface { } , len ( new ) )
for kNew , vNew := range newLookupMap {
if _ , ok := oldLookupMap [ kNew ] ; ! ok {
addedRoles [ string ( kNew ) ] = vNew
}
}
updatedRoles := make ( map [ string ] interface { } , len ( new ) )
unchangedRoles := make ( map [ string ] interface { } , len ( new ) )
for kOld , vOld := range oldLookupMap {
if vNew , ok := newLookupMap [ kOld ] ; ok {
if reflect . DeepEqual ( vOld , vNew ) {
unchangedRoles [ string ( kOld ) ] = vOld
} else {
updatedRoles [ string ( kOld ) ] = [ ] interface { } { vOld , vNew }
}
}
}
return droppedRoles , addedRoles , updatedRoles , unchangedRoles
}
func schemaPolicyToHCL ( s * acl . Schema ) map [ string ] interface { } {
return map [ string ] interface { } {
schemaPolicyRoleAttr : s . Role ,
schemaPolicyCreateAttr : s . GetPrivilege ( acl . Create ) ,
schemaPolicyCreateWithGrantAttr : s . GetGrantOption ( acl . Create ) ,
schemaPolicyUsageAttr : s . GetPrivilege ( acl . Usage ) ,
schemaPolicyUsageWithGrantAttr : s . GetGrantOption ( acl . Usage ) ,
}
}
func schemaPolicyToACL ( policyMap map [ string ] interface { } ) acl . Schema {
var rolePolicy acl . Schema
if policyMap [ schemaPolicyCreateAttr ] . ( bool ) {
rolePolicy . Privileges |= acl . Create
}
if policyMap [ schemaPolicyCreateWithGrantAttr ] . ( bool ) {
rolePolicy . Privileges |= acl . Create
rolePolicy . GrantOptions |= acl . Create
}
if policyMap [ schemaPolicyUsageAttr ] . ( bool ) {
rolePolicy . Privileges |= acl . Usage
}
if policyMap [ schemaPolicyUsageWithGrantAttr ] . ( bool ) {
rolePolicy . Privileges |= acl . Usage
rolePolicy . GrantOptions |= acl . Usage
}
if roleRaw , ok := policyMap [ schemaPolicyRoleAttr ] ; ok {
rolePolicy . Role = roleRaw . ( string )
}
return rolePolicy
}