github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/postgresql/resource_postgresql_schema.go (about)

     1  package postgresql
     2  
     3  import (
     4  	"bytes"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"reflect"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/errwrap"
    13  	"github.com/hashicorp/terraform/helper/schema"
    14  	"github.com/lib/pq"
    15  	"github.com/sean-/postgresql-acl"
    16  )
    17  
    18  const (
    19  	schemaNameAttr    = "name"
    20  	schemaOwnerAttr   = "owner"
    21  	schemaPolicyAttr  = "policy"
    22  	schemaIfNotExists = "if_not_exists"
    23  
    24  	schemaPolicyCreateAttr          = "create"
    25  	schemaPolicyCreateWithGrantAttr = "create_with_grant"
    26  	schemaPolicyRoleAttr            = "role"
    27  	schemaPolicyUsageAttr           = "usage"
    28  	schemaPolicyUsageWithGrantAttr  = "usage_with_grant"
    29  )
    30  
    31  func resourcePostgreSQLSchema() *schema.Resource {
    32  	return &schema.Resource{
    33  		Create: resourcePostgreSQLSchemaCreate,
    34  		Read:   resourcePostgreSQLSchemaRead,
    35  		Update: resourcePostgreSQLSchemaUpdate,
    36  		Delete: resourcePostgreSQLSchemaDelete,
    37  		Exists: resourcePostgreSQLSchemaExists,
    38  		Importer: &schema.ResourceImporter{
    39  			State: schema.ImportStatePassthrough,
    40  		},
    41  
    42  		Schema: map[string]*schema.Schema{
    43  			schemaNameAttr: {
    44  				Type:        schema.TypeString,
    45  				Required:    true,
    46  				Description: "The name of the schema",
    47  			},
    48  			schemaOwnerAttr: {
    49  				Type:        schema.TypeString,
    50  				Optional:    true,
    51  				Computed:    true,
    52  				Description: "The ROLE name who owns the schema",
    53  			},
    54  			schemaIfNotExists: {
    55  				Type:        schema.TypeBool,
    56  				Optional:    true,
    57  				Default:     true,
    58  				Description: "When true, use the existing schema if it exsts",
    59  			},
    60  			schemaPolicyAttr: &schema.Schema{
    61  				Type:     schema.TypeSet,
    62  				Optional: true,
    63  				Computed: true,
    64  				Elem: &schema.Resource{
    65  					Schema: map[string]*schema.Schema{
    66  						schemaPolicyCreateAttr: {
    67  							Type:          schema.TypeBool,
    68  							Optional:      true,
    69  							Default:       false,
    70  							Description:   "If true, allow the specified ROLEs to CREATE new objects within the schema(s)",
    71  							ConflictsWith: []string{schemaPolicyAttr + "." + schemaPolicyCreateWithGrantAttr},
    72  						},
    73  						schemaPolicyCreateWithGrantAttr: {
    74  							Type:          schema.TypeBool,
    75  							Optional:      true,
    76  							Default:       false,
    77  							Description:   "If true, allow the specified ROLEs to CREATE new objects within the schema(s) and GRANT the same CREATE privilege to different ROLEs",
    78  							ConflictsWith: []string{schemaPolicyAttr + "." + schemaPolicyCreateAttr},
    79  						},
    80  						schemaPolicyRoleAttr: {
    81  							Type:        schema.TypeString,
    82  							Elem:        &schema.Schema{Type: schema.TypeString},
    83  							Optional:    true,
    84  							Default:     "",
    85  							Description: "ROLE who will receive this policy (default: PUBLIC)",
    86  						},
    87  						schemaPolicyUsageAttr: {
    88  							Type:          schema.TypeBool,
    89  							Optional:      true,
    90  							Default:       false,
    91  							Description:   "If true, allow the specified ROLEs to use objects within the schema(s)",
    92  							ConflictsWith: []string{schemaPolicyAttr + "." + schemaPolicyUsageWithGrantAttr},
    93  						},
    94  						schemaPolicyUsageWithGrantAttr: {
    95  							Type:          schema.TypeBool,
    96  							Optional:      true,
    97  							Default:       false,
    98  							Description:   "If true, allow the specified ROLEs to use objects within the schema(s) and GRANT the same USAGE privilege to different ROLEs",
    99  							ConflictsWith: []string{schemaPolicyAttr + "." + schemaPolicyUsageAttr},
   100  						},
   101  					},
   102  				},
   103  			},
   104  		},
   105  	}
   106  }
   107  
   108  func resourcePostgreSQLSchemaCreate(d *schema.ResourceData, meta interface{}) error {
   109  	c := meta.(*Client)
   110  
   111  	queries := []string{}
   112  
   113  	schemaName := d.Get(schemaNameAttr).(string)
   114  	{
   115  		b := bytes.NewBufferString("CREATE SCHEMA ")
   116  		if v := d.Get(schemaIfNotExists); v.(bool) {
   117  			fmt.Fprint(b, "IF NOT EXISTS ")
   118  		}
   119  		fmt.Fprint(b, pq.QuoteIdentifier(schemaName))
   120  
   121  		switch v, ok := d.GetOk(schemaOwnerAttr); {
   122  		case ok:
   123  			fmt.Fprint(b, " AUTHORIZATION ", pq.QuoteIdentifier(v.(string)))
   124  		}
   125  		queries = append(queries, b.String())
   126  	}
   127  
   128  	// ACL objects that can generate the necessary SQL
   129  	type RoleKey string
   130  	var schemaPolicies map[RoleKey]acl.Schema
   131  
   132  	if policiesRaw, ok := d.GetOk(schemaPolicyAttr); ok {
   133  		policiesList := policiesRaw.(*schema.Set).List()
   134  
   135  		// NOTE: len(policiesList) doesn't take into account multiple
   136  		// roles per policy.
   137  		schemaPolicies = make(map[RoleKey]acl.Schema, len(policiesList))
   138  
   139  		for _, policyRaw := range policiesList {
   140  			policyMap := policyRaw.(map[string]interface{})
   141  			rolePolicy := schemaPolicyToACL(policyMap)
   142  
   143  			roleKey := RoleKey(strings.ToLower(rolePolicy.Role))
   144  			if existingRolePolicy, ok := schemaPolicies[roleKey]; ok {
   145  				schemaPolicies[roleKey] = existingRolePolicy.Merge(rolePolicy)
   146  			} else {
   147  				schemaPolicies[roleKey] = rolePolicy
   148  			}
   149  		}
   150  	}
   151  
   152  	for _, policy := range schemaPolicies {
   153  		queries = append(queries, policy.Grants(schemaName)...)
   154  	}
   155  
   156  	c.catalogLock.Lock()
   157  	defer c.catalogLock.Unlock()
   158  
   159  	conn, err := c.Connect()
   160  	if err != nil {
   161  		return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
   162  	}
   163  	defer conn.Close()
   164  
   165  	txn, err := conn.Begin()
   166  	if err != nil {
   167  		return err
   168  	}
   169  	defer txn.Rollback()
   170  
   171  	for _, query := range queries {
   172  		_, err = txn.Query(query)
   173  		if err != nil {
   174  			return errwrap.Wrapf(fmt.Sprintf("Error creating schema %s: {{err}}", schemaName), err)
   175  		}
   176  	}
   177  
   178  	if err := txn.Commit(); err != nil {
   179  		return errwrap.Wrapf("Error committing schema: {{err}}", err)
   180  	}
   181  
   182  	d.SetId(schemaName)
   183  
   184  	return resourcePostgreSQLSchemaReadImpl(d, meta)
   185  }
   186  
   187  func resourcePostgreSQLSchemaDelete(d *schema.ResourceData, meta interface{}) error {
   188  	c := meta.(*Client)
   189  	c.catalogLock.Lock()
   190  	defer c.catalogLock.Unlock()
   191  
   192  	conn, err := c.Connect()
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer conn.Close()
   197  
   198  	txn, err := conn.Begin()
   199  	if err != nil {
   200  		return err
   201  	}
   202  	defer txn.Rollback()
   203  
   204  	schemaName := d.Get(schemaNameAttr).(string)
   205  
   206  	// NOTE(sean@): Deliberately not performing a cascading drop.
   207  	query := fmt.Sprintf("DROP SCHEMA %s", pq.QuoteIdentifier(schemaName))
   208  	_, err = txn.Query(query)
   209  	if err != nil {
   210  		return errwrap.Wrapf("Error deleting schema: {{err}}", err)
   211  	}
   212  
   213  	if err := txn.Commit(); err != nil {
   214  		return errwrap.Wrapf("Error committing schema: {{err}}", err)
   215  	}
   216  
   217  	d.SetId("")
   218  
   219  	return nil
   220  }
   221  
   222  func resourcePostgreSQLSchemaExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   223  	c := meta.(*Client)
   224  	c.catalogLock.RLock()
   225  	defer c.catalogLock.RUnlock()
   226  
   227  	conn, err := c.Connect()
   228  	if err != nil {
   229  		return false, err
   230  	}
   231  	defer conn.Close()
   232  
   233  	var schemaName string
   234  	err = conn.QueryRow("SELECT n.nspname FROM pg_catalog.pg_namespace n WHERE n.nspname=$1", d.Id()).Scan(&schemaName)
   235  	switch {
   236  	case err == sql.ErrNoRows:
   237  		return false, nil
   238  	case err != nil:
   239  		return false, errwrap.Wrapf("Error reading schema: {{err}}", err)
   240  	}
   241  
   242  	return true, nil
   243  }
   244  
   245  func resourcePostgreSQLSchemaRead(d *schema.ResourceData, meta interface{}) error {
   246  	c := meta.(*Client)
   247  	c.catalogLock.RLock()
   248  	defer c.catalogLock.RUnlock()
   249  
   250  	return resourcePostgreSQLSchemaReadImpl(d, meta)
   251  }
   252  
   253  func resourcePostgreSQLSchemaReadImpl(d *schema.ResourceData, meta interface{}) error {
   254  	c := meta.(*Client)
   255  	conn, err := c.Connect()
   256  	if err != nil {
   257  		return err
   258  	}
   259  	defer conn.Close()
   260  
   261  	schemaId := d.Id()
   262  	var schemaName, schemaOwner string
   263  	var schemaACLs []string
   264  	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))
   265  	switch {
   266  	case err == sql.ErrNoRows:
   267  		log.Printf("[WARN] PostgreSQL schema (%s) not found", schemaId)
   268  		d.SetId("")
   269  		return nil
   270  	case err != nil:
   271  		return errwrap.Wrapf("Error reading schema: {{err}}", err)
   272  	default:
   273  		type RoleKey string
   274  		schemaPolicies := make(map[RoleKey]acl.Schema, len(schemaACLs))
   275  		for _, aclStr := range schemaACLs {
   276  			aclItem, err := acl.Parse(aclStr)
   277  			if err != nil {
   278  				return errwrap.Wrapf("Error parsing aclitem: {{err}}", err)
   279  			}
   280  
   281  			schemaACL, err := acl.NewSchema(aclItem)
   282  			if err != nil {
   283  				return errwrap.Wrapf("invalid perms for schema: {{err}}", err)
   284  			}
   285  
   286  			roleKey := RoleKey(strings.ToLower(schemaACL.Role))
   287  			var mergedPolicy acl.Schema
   288  			if existingRolePolicy, ok := schemaPolicies[roleKey]; ok {
   289  				mergedPolicy = existingRolePolicy.Merge(schemaACL)
   290  			} else {
   291  				mergedPolicy = schemaACL
   292  			}
   293  			schemaPolicies[roleKey] = mergedPolicy
   294  		}
   295  
   296  		d.Set(schemaNameAttr, schemaName)
   297  		d.Set(schemaOwnerAttr, schemaOwner)
   298  		d.SetId(schemaName)
   299  		return nil
   300  	}
   301  }
   302  
   303  func resourcePostgreSQLSchemaUpdate(d *schema.ResourceData, meta interface{}) error {
   304  	c := meta.(*Client)
   305  	c.catalogLock.Lock()
   306  	defer c.catalogLock.Unlock()
   307  
   308  	conn, err := c.Connect()
   309  	if err != nil {
   310  		return err
   311  	}
   312  	defer conn.Close()
   313  
   314  	txn, err := conn.Begin()
   315  	if err != nil {
   316  		return err
   317  	}
   318  	defer txn.Rollback()
   319  
   320  	if err := setSchemaName(txn, d); err != nil {
   321  		return err
   322  	}
   323  
   324  	if err := setSchemaOwner(txn, d); err != nil {
   325  		return err
   326  	}
   327  
   328  	if err := setSchemaPolicy(txn, d); err != nil {
   329  		return err
   330  	}
   331  
   332  	if err := txn.Commit(); err != nil {
   333  		return errwrap.Wrapf("Error committing schema: {{err}}", err)
   334  	}
   335  
   336  	return resourcePostgreSQLSchemaReadImpl(d, meta)
   337  }
   338  
   339  func setSchemaName(txn *sql.Tx, d *schema.ResourceData) error {
   340  	if !d.HasChange(schemaNameAttr) {
   341  		return nil
   342  	}
   343  
   344  	oraw, nraw := d.GetChange(schemaNameAttr)
   345  	o := oraw.(string)
   346  	n := nraw.(string)
   347  	if n == "" {
   348  		return errors.New("Error setting schema name to an empty string")
   349  	}
   350  
   351  	query := fmt.Sprintf("ALTER SCHEMA %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
   352  	if _, err := txn.Query(query); err != nil {
   353  		return errwrap.Wrapf("Error updating schema NAME: {{err}}", err)
   354  	}
   355  	d.SetId(n)
   356  
   357  	return nil
   358  }
   359  
   360  func setSchemaOwner(txn *sql.Tx, d *schema.ResourceData) error {
   361  	if !d.HasChange(schemaOwnerAttr) {
   362  		return nil
   363  	}
   364  
   365  	oraw, nraw := d.GetChange(schemaOwnerAttr)
   366  	o := oraw.(string)
   367  	n := nraw.(string)
   368  	if n == "" {
   369  		return errors.New("Error setting schema owner to an empty string")
   370  	}
   371  
   372  	query := fmt.Sprintf("ALTER SCHEMA %s OWNER TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
   373  	if _, err := txn.Query(query); err != nil {
   374  		return errwrap.Wrapf("Error updating schema OWNER: {{err}}", err)
   375  	}
   376  
   377  	return nil
   378  }
   379  
   380  func setSchemaPolicy(txn *sql.Tx, d *schema.ResourceData) error {
   381  	if !d.HasChange(schemaPolicyAttr) {
   382  		return nil
   383  	}
   384  
   385  	schemaName := d.Get(schemaNameAttr).(string)
   386  
   387  	oraw, nraw := d.GetChange(schemaPolicyAttr)
   388  	oldList := oraw.(*schema.Set).List()
   389  	newList := nraw.(*schema.Set).List()
   390  	queries := make([]string, 0, len(oldList)+len(newList))
   391  	dropped, added, updated, _ := schemaChangedPolicies(oldList, newList)
   392  
   393  	for _, p := range dropped {
   394  		pMap := p.(map[string]interface{})
   395  		rolePolicy := schemaPolicyToACL(pMap)
   396  
   397  		// The PUBLIC role can not be DROP'ed, therefore we do not need
   398  		// to prevent revoking against it not existing.
   399  		if rolePolicy.Role != "" {
   400  			var foundUser bool
   401  			err := txn.QueryRow(`SELECT TRUE FROM pg_catalog.pg_user WHERE usename = $1`, rolePolicy.Role).Scan(&foundUser)
   402  			switch {
   403  			case err == sql.ErrNoRows:
   404  				// Don't execute this role's REVOKEs because the role
   405  				// was dropped first and therefore doesn't exist.
   406  			case err != nil:
   407  				return errwrap.Wrapf("Error reading schema: {{err}}", err)
   408  			default:
   409  				queries = append(queries, rolePolicy.Revokes(schemaName)...)
   410  			}
   411  		}
   412  	}
   413  
   414  	for _, p := range added {
   415  		pMap := p.(map[string]interface{})
   416  		rolePolicy := schemaPolicyToACL(pMap)
   417  		queries = append(queries, rolePolicy.Grants(schemaName)...)
   418  	}
   419  
   420  	for _, p := range updated {
   421  		policies := p.([]interface{})
   422  		if len(policies) != 2 {
   423  			panic("expected 2 policies, old and new")
   424  		}
   425  
   426  		{
   427  			oldPolicies := policies[0].(map[string]interface{})
   428  			rolePolicy := schemaPolicyToACL(oldPolicies)
   429  			queries = append(queries, rolePolicy.Revokes(schemaName)...)
   430  		}
   431  
   432  		{
   433  			newPolicies := policies[1].(map[string]interface{})
   434  			rolePolicy := schemaPolicyToACL(newPolicies)
   435  			queries = append(queries, rolePolicy.Grants(schemaName)...)
   436  		}
   437  	}
   438  
   439  	for _, query := range queries {
   440  		if _, err := txn.Query(query); err != nil {
   441  			return errwrap.Wrapf("Error updating schema DCL: {{err}}", err)
   442  		}
   443  	}
   444  
   445  	return nil
   446  }
   447  
   448  // schemaChangedPolicies walks old and new to create a set of queries that can
   449  // be executed to enact each type of state change (roles that have been dropped
   450  // from the policy, added to a policy, have updated privilges, or are
   451  // unchanged).
   452  func schemaChangedPolicies(old, new []interface{}) (dropped, added, update, unchanged map[string]interface{}) {
   453  	type RoleKey string
   454  	oldLookupMap := make(map[RoleKey]interface{}, len(old))
   455  	for idx, _ := range old {
   456  		v := old[idx]
   457  		schemaPolicy := v.(map[string]interface{})
   458  		if roleRaw, ok := schemaPolicy[schemaPolicyRoleAttr]; ok {
   459  			role := roleRaw.(string)
   460  			roleKey := strings.ToLower(role)
   461  			oldLookupMap[RoleKey(roleKey)] = schemaPolicy
   462  		}
   463  	}
   464  
   465  	newLookupMap := make(map[RoleKey]interface{}, len(new))
   466  	for idx, _ := range new {
   467  		v := new[idx]
   468  		schemaPolicy := v.(map[string]interface{})
   469  		if roleRaw, ok := schemaPolicy[schemaPolicyRoleAttr]; ok {
   470  			role := roleRaw.(string)
   471  			roleKey := strings.ToLower(role)
   472  			newLookupMap[RoleKey(roleKey)] = schemaPolicy
   473  		}
   474  	}
   475  
   476  	droppedRoles := make(map[string]interface{}, len(old))
   477  	for kOld, vOld := range oldLookupMap {
   478  		if _, ok := newLookupMap[kOld]; !ok {
   479  			droppedRoles[string(kOld)] = vOld
   480  		}
   481  	}
   482  
   483  	addedRoles := make(map[string]interface{}, len(new))
   484  	for kNew, vNew := range newLookupMap {
   485  		if _, ok := oldLookupMap[kNew]; !ok {
   486  			addedRoles[string(kNew)] = vNew
   487  		}
   488  	}
   489  
   490  	updatedRoles := make(map[string]interface{}, len(new))
   491  	unchangedRoles := make(map[string]interface{}, len(new))
   492  	for kOld, vOld := range oldLookupMap {
   493  		if vNew, ok := newLookupMap[kOld]; ok {
   494  			if reflect.DeepEqual(vOld, vNew) {
   495  				unchangedRoles[string(kOld)] = vOld
   496  			} else {
   497  				updatedRoles[string(kOld)] = []interface{}{vOld, vNew}
   498  			}
   499  		}
   500  	}
   501  
   502  	return droppedRoles, addedRoles, updatedRoles, unchangedRoles
   503  }
   504  
   505  func schemaPolicyToHCL(s *acl.Schema) map[string]interface{} {
   506  	return map[string]interface{}{
   507  		schemaPolicyRoleAttr:            s.Role,
   508  		schemaPolicyCreateAttr:          s.GetPrivilege(acl.Create),
   509  		schemaPolicyCreateWithGrantAttr: s.GetGrantOption(acl.Create),
   510  		schemaPolicyUsageAttr:           s.GetPrivilege(acl.Usage),
   511  		schemaPolicyUsageWithGrantAttr:  s.GetGrantOption(acl.Usage),
   512  	}
   513  }
   514  
   515  func schemaPolicyToACL(policyMap map[string]interface{}) acl.Schema {
   516  	var rolePolicy acl.Schema
   517  
   518  	if policyMap[schemaPolicyCreateAttr].(bool) {
   519  		rolePolicy.Privileges |= acl.Create
   520  	}
   521  
   522  	if policyMap[schemaPolicyCreateWithGrantAttr].(bool) {
   523  		rolePolicy.Privileges |= acl.Create
   524  		rolePolicy.GrantOptions |= acl.Create
   525  	}
   526  
   527  	if policyMap[schemaPolicyUsageAttr].(bool) {
   528  		rolePolicy.Privileges |= acl.Usage
   529  	}
   530  
   531  	if policyMap[schemaPolicyUsageWithGrantAttr].(bool) {
   532  		rolePolicy.Privileges |= acl.Usage
   533  		rolePolicy.GrantOptions |= acl.Usage
   534  	}
   535  
   536  	if roleRaw, ok := policyMap[schemaPolicyRoleAttr]; ok {
   537  		rolePolicy.Role = roleRaw.(string)
   538  	}
   539  
   540  	return rolePolicy
   541  }