Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #333 (2020-12-07 21:18:29)

[refactor] Grants: Wrap grants with their valid privilges for cztack codegen
<!-- Feel free to delete comments as you fill this in -->

<!-- summary of changes -->
Wrapping some grant resources with more context so that it can be consumed for cogegen terraform modules in cztack. In particular, trying to automate the generate of the ALL grant modules for all types the provider supports. 

See https://github.com/chanzuckerberg/cztack/pull/262 for more context

## Test Plan
<!-- detail ways in which this PR has been tested or needs to be tested -->
* [ ] acceptance tests
<!-- add more below if you think they are relevant -->
* [ ] …

## References
<!-- issues documentation links, etc  -->

* 


Patch:
diff --git a/docs/index.md b/docs/index.md
index b6f2e40b..b40f88bb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -32,19 +32,16 @@ provider snowflake {
 
 ## Schema
 
-### Required
-
-- **account** (String, Required)
-- **username** (String, Required)
-
 ### Optional
 
+- **account** (String, Optional)
 - **browser_auth** (Boolean, Optional)
 - **oauth_access_token** (String, Optional)
 - **password** (String, Optional)
 - **private_key_path** (String, Optional)
 - **region** (String, Optional)
 - **role** (String, Optional)
+- **username** (String, Optional)
 
 
 ## Authentication
diff --git a/docs/resources/account_grant.md b/docs/resources/account_grant.md
index 6b9c0e01..ef636fc0 100644
--- a/docs/resources/account_grant.md
+++ b/docs/resources/account_grant.md
@@ -24,7 +24,7 @@ resource snowflake_account_grant grant {
 ### Optional
 
 - **id** (String, Optional) The ID of this resource.
-- **privilege** (String, Optional) The privilege to grant on the schema.
+- **privilege** (String, Optional) The privilege to grant on the account.
 - **roles** (Set of String, Optional) Grants privilege to these roles.
 - **with_grant_option** (Boolean, Optional) When this is set to true, allows the recipient role to grant the privileges to other roles.
 
diff --git a/go.mod b/go.mod
index 8c650f06..d8e3e4ef 100644
--- a/go.mod
+++ b/go.mod
@@ -15,5 +15,5 @@ require (
 	github.com/snowflakedb/gosnowflake v1.3.12
 	github.com/stretchr/testify v1.6.1
 	golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9
-	golang.org/x/tools v0.0.0-20201208211828-de58e7c01d49
+	golang.org/x/tools v0.0.0-20201208225042-ef0c635082b3
 )
diff --git a/go.sum b/go.sum
index f4476ed2..0fb1d1f2 100644
--- a/go.sum
+++ b/go.sum
@@ -937,8 +937,8 @@ golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20201208211828-de58e7c01d49 h1:K1QAOVIWIvmQ66F1Z3AEa9Wzp0bj+xU3YzLkvROk2Ds=
-golang.org/x/tools v0.0.0-20201208211828-de58e7c01d49/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208225042-ef0c635082b3 h1:ZCTyS0mmKNKnJH/wbIdBVEe+mhkxEgV6lDU9G4ZYH6M=
+golang.org/x/tools v0.0.0-20201208225042-ef0c635082b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 0629d632..004fa594 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -67,35 +67,7 @@ func Provider() *schema.Provider {
 				DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_REGION", "us-west-2"),
 			},
 		},
-		ResourcesMap: map[string]*schema.Resource{
-			"snowflake_account_grant":             resources.AccountGrant(),
-			"snowflake_database":                  resources.Database(),
-			"snowflake_database_grant":            resources.DatabaseGrant(),
-			"snowflake_integration_grant":         resources.IntegrationGrant(),
-			"snowflake_managed_account":           resources.ManagedAccount(),
-			"snowflake_network_policy":            resources.NetworkPolicy(),
-			"snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(),
-			"snowflake_pipe":                      resources.Pipe(),
-			"snowflake_resource_monitor":          resources.ResourceMonitor(),
-			"snowflake_resource_monitor_grant":    resources.ResourceMonitorGrant(),
-			"snowflake_role":                      resources.Role(),
-			"snowflake_role_grants":               resources.RoleGrants(),
-			"snowflake_schema":                    resources.Schema(),
-			"snowflake_schema_grant":              resources.SchemaGrant(),
-			"snowflake_share":                     resources.Share(),
-			"snowflake_stage":                     resources.Stage(),
-			"snowflake_stage_grant":               resources.StageGrant(),
-			"snowflake_storage_integration":       resources.StorageIntegration(),
-			"snowflake_stream":                    resources.Stream(),
-			"snowflake_user":                      resources.User(),
-			"snowflake_view":                      resources.View(),
-			"snowflake_view_grant":                resources.ViewGrant(),
-			"snowflake_task":                      resources.Task(),
-			"snowflake_table":                     resources.Table(),
-			"snowflake_table_grant":               resources.TableGrant(),
-			"snowflake_warehouse":                 resources.Warehouse(),
-			"snowflake_warehouse_grant":           resources.WarehouseGrant(),
-		},
+		ResourcesMap: getResources(),
 		DataSourcesMap: map[string]*schema.Resource{
 			"snowflake_system_get_aws_sns_iam_policy": datasources.SystemGetAWSSNSIAMPolicy(),
 		},
@@ -103,6 +75,49 @@ func Provider() *schema.Provider {
 	}
 }
 
+func GetGrantResources() resources.TerraformGrantResources {
+	grants := resources.TerraformGrantResources{
+		"snowflake_account_grant":          resources.AccountGrant(),
+		"snowflake_database_grant":         resources.DatabaseGrant(),
+		"snowflake_integration_grant":      resources.IntegrationGrant(),
+		"snowflake_resource_monitor_grant": resources.ResourceMonitorGrant(),
+		"snowflake_schema_grant":           resources.SchemaGrant(),
+		"snowflake_stage_grant":            resources.StageGrant(),
+		"snowflake_table_grant":            resources.TableGrant(),
+		"snowflake_view_grant":             resources.ViewGrant(),
+		"snowflake_warehouse_grant":        resources.WarehouseGrant(),
+	}
+	return grants
+}
+
+func getResources() map[string]*schema.Resource {
+	others := map[string]*schema.Resource{
+		"snowflake_database":                  resources.Database(),
+		"snowflake_managed_account":           resources.ManagedAccount(),
+		"snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(),
+		"snowflake_network_policy":            resources.NetworkPolicy(),
+		"snowflake_pipe":                      resources.Pipe(),
+		"snowflake_resource_monitor":          resources.ResourceMonitor(),
+		"snowflake_role_grants":               resources.RoleGrants(),
+		"snowflake_role":                      resources.Role(),
+		"snowflake_schema":                    resources.Schema(),
+		"snowflake_share":                     resources.Share(),
+		"snowflake_stage":                     resources.Stage(),
+		"snowflake_storage_integration":       resources.StorageIntegration(),
+		"snowflake_stream":                    resources.Stream(),
+		"snowflake_table":                     resources.Table(),
+		"snowflake_task":                      resources.Task(),
+		"snowflake_user":                      resources.User(),
+		"snowflake_view":                      resources.View(),
+		"snowflake_warehouse":                 resources.Warehouse(),
+	}
+
+	return mergeSchemas(
+		others,
+		GetGrantResources().GetTfSchemas(),
+	)
+}
+
 func ConfigureProvider(s *schema.ResourceData) (interface{}, error) {
 	account := s.Get("account").(string)
 	user := s.Get("username").(string)
diff --git a/pkg/provider/provider_helper.go b/pkg/provider/provider_helper.go
new file mode 100644
index 00000000..2c69d7f8
--- /dev/null
+++ b/pkg/provider/provider_helper.go
@@ -0,0 +1,13 @@
+package provider
+
+import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+func mergeSchemas(schemaCollections ...map[string]*schema.Resource) map[string]*schema.Resource {
+	out := map[string]*schema.Resource{}
+	for _, schemaCollection := range schemaCollections {
+		for name, s := range schemaCollection {
+			out[name] = s
+		}
+	}
+	return out
+}
diff --git a/pkg/resources/account_grant.go b/pkg/resources/account_grant.go
index a433d85d..4242bdad 100644
--- a/pkg/resources/account_grant.go
+++ b/pkg/resources/account_grant.go
@@ -1,10 +1,9 @@
 package resources
 
 import (
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-
-	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 )
 
 var validAccountPrivileges = NewPrivilegeSet(
@@ -23,9 +22,9 @@ var accountGrantSchema = map[string]*schema.Schema{
 	"privilege": {
 		Type:         schema.TypeString,
 		Optional:     true,
-		Description:  "The privilege to grant on the schema.",
+		Description:  "The privilege to grant on the account.",
 		Default:      privilegeMonitorUsage,
-		ValidateFunc: validation.StringInSlice(validAccountPrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validAccountPrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -44,14 +43,17 @@ var accountGrantSchema = map[string]*schema.Schema{
 	},
 }
 
-// ViewGrant returns a pointer to the resource representing a view grant
-func AccountGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateAccountGrant,
-		Read:   ReadAccountGrant,
-		Delete: DeleteAccountGrant,
-
-		Schema: accountGrantSchema,
+// AccountGrant returns a pointer to the resource representing an account grant
+func AccountGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateAccountGrant,
+			Read:   ReadAccountGrant,
+			Delete: DeleteAccountGrant,
+
+			Schema: accountGrantSchema,
+		},
+		ValidPrivs: validAccountPrivileges,
 	}
 }
 
diff --git a/pkg/resources/database_grant.go b/pkg/resources/database_grant.go
index 0f117a5e..c9278b14 100644
--- a/pkg/resources/database_grant.go
+++ b/pkg/resources/database_grant.go
@@ -6,7 +6,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 )
 
-var ValidDatabasePrivileges = NewPrivilegeSet(
+var validDatabasePrivileges = NewPrivilegeSet(
 	privilegeCreateSchema,
 	privilegeImportedPrivileges,
 	privilegeModify,
@@ -28,7 +28,7 @@ var databaseGrantSchema = map[string]*schema.Schema{
 		Optional:     true,
 		Description:  "The privilege to grant on the database.",
 		Default:      "USAGE",
-		ValidateFunc: validation.StringInSlice(ValidDatabasePrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validDatabasePrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -55,16 +55,19 @@ var databaseGrantSchema = map[string]*schema.Schema{
 }
 
 // DatabaseGrant returns a pointer to the resource representing a database grant
-func DatabaseGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateDatabaseGrant,
-		Read:   ReadDatabaseGrant,
-		Delete: DeleteDatabaseGrant,
+func DatabaseGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateDatabaseGrant,
+			Read:   ReadDatabaseGrant,
+			Delete: DeleteDatabaseGrant,
 
-		Schema: databaseGrantSchema,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+			Schema: databaseGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validDatabasePrivileges,
 	}
 }
 
@@ -120,7 +123,7 @@ func ReadDatabaseGrant(d *schema.ResourceData, meta interface{}) error {
 	}
 
 	builder := snowflake.DatabaseGrant(grantID.ResourceName)
-	return readGenericGrant(d, meta, databaseGrantSchema, builder, false, ValidDatabasePrivileges)
+	return readGenericGrant(d, meta, databaseGrantSchema, builder, false, validDatabasePrivileges)
 }
 
 // DeleteDatabaseGrant implements schema.DeleteFunc
diff --git a/pkg/resources/grant_helpers.go b/pkg/resources/grant_helpers.go
index c8b2aa0b..526d4837 100644
--- a/pkg/resources/grant_helpers.go
+++ b/pkg/resources/grant_helpers.go
@@ -14,6 +14,22 @@ import (
 	"github.com/jmoiron/sqlx"
 )
 
+// TerraformGrantResource augments terraform's *schema.Resource with extra context
+type TerraformGrantResource struct {
+	Resource   *schema.Resource
+	ValidPrivs PrivilegeSet
+}
+
+type TerraformGrantResources map[string]*TerraformGrantResource
+
+func (t TerraformGrantResources) GetTfSchemas() map[string]*schema.Resource {
+	out := map[string]*schema.Resource{}
+	for name, grant := range t {
+		out[name] = grant.Resource
+	}
+	return out
+}
+
 const (
 	grantIDDelimiter = '|'
 )
diff --git a/pkg/resources/integration_grant.go b/pkg/resources/integration_grant.go
index e299626c..6438bc2d 100644
--- a/pkg/resources/integration_grant.go
+++ b/pkg/resources/integration_grant.go
@@ -22,7 +22,7 @@ var integrationGrantSchema = map[string]*schema.Schema{
 		Optional:     true,
 		Description:  "The privilege to grant on the integration.",
 		Default:      "USAGE",
-		ValidateFunc: validation.StringInSlice(validIntegrationPrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validIntegrationPrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -42,13 +42,16 @@ var integrationGrantSchema = map[string]*schema.Schema{
 }
 
 // IntegrationGrant returns a pointer to the resource representing a integration grant
-func IntegrationGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateIntegrationGrant,
-		Read:   ReadIntegrationGrant,
-		Delete: DeleteIntegrationGrant,
+func IntegrationGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateIntegrationGrant,
+			Read:   ReadIntegrationGrant,
+			Delete: DeleteIntegrationGrant,
 
-		Schema: integrationGrantSchema,
+			Schema: integrationGrantSchema,
+		},
+		ValidPrivs: validIntegrationPrivileges,
 	}
 }
 
diff --git a/pkg/resources/privileges.go b/pkg/resources/privileges.go
index a156ef0b..f670d5ac 100644
--- a/pkg/resources/privileges.go
+++ b/pkg/resources/privileges.go
@@ -59,7 +59,7 @@ func NewPrivilegeSet(privileges ...Privilege) PrivilegeSet {
 	return ps
 }
 
-func (ps PrivilegeSet) toList() []string {
+func (ps PrivilegeSet) ToList() []string {
 	privs := []string{}
 	for p := range ps {
 		privs = append(privs, string(p))
diff --git a/pkg/resources/resource_monitor_grant.go b/pkg/resources/resource_monitor_grant.go
index 117e749f..2de89870 100644
--- a/pkg/resources/resource_monitor_grant.go
+++ b/pkg/resources/resource_monitor_grant.go
@@ -23,7 +23,7 @@ var resourceMonitorGrantSchema = map[string]*schema.Schema{
 		Optional:     true,
 		Description:  "The privilege to grant on the resource monitor.",
 		Default:      "MONITOR",
-		ValidateFunc: validation.StringInSlice(validResourceMonitorPrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validResourceMonitorPrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -43,13 +43,16 @@ var resourceMonitorGrantSchema = map[string]*schema.Schema{
 }
 
 // ResourceMonitorGrant returns a pointer to the resource representing a resource monitor grant
-func ResourceMonitorGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateResourceMonitorGrant,
-		Read:   ReadResourceMonitorGrant,
-		Delete: DeleteResourceMonitorGrant,
+func ResourceMonitorGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateResourceMonitorGrant,
+			Read:   ReadResourceMonitorGrant,
+			Delete: DeleteResourceMonitorGrant,
 
-		Schema: resourceMonitorGrantSchema,
+			Schema: resourceMonitorGrantSchema,
+		},
+		ValidPrivs: validResourceMonitorPrivileges,
 	}
 }
 
diff --git a/pkg/resources/schema_grant.go b/pkg/resources/schema_grant.go
index b8b74e83..18b25809 100644
--- a/pkg/resources/schema_grant.go
+++ b/pkg/resources/schema_grant.go
@@ -47,7 +47,7 @@ var schemaGrantSchema = map[string]*schema.Schema{
 		Optional:     true,
 		Description:  "The privilege to grant on the current or future schema. Note that if \"OWNERSHIP\" is specified, ensure that the role that terraform is using is granted access.",
 		Default:      "USAGE",
-		ValidateFunc: validation.StringInSlice(validSchemaPrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validSchemaPrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -82,16 +82,19 @@ var schemaGrantSchema = map[string]*schema.Schema{
 }
 
 // SchemaGrant returns a pointer to the resource representing a view grant
-func SchemaGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateSchemaGrant,
-		Read:   ReadSchemaGrant,
-		Delete: DeleteSchemaGrant,
-
-		Schema: schemaGrantSchema,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+func SchemaGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateSchemaGrant,
+			Read:   ReadSchemaGrant,
+			Delete: DeleteSchemaGrant,
+
+			Schema: schemaGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validSchemaPrivileges,
 	}
 }
 
diff --git a/pkg/resources/stage_grant.go b/pkg/resources/stage_grant.go
index 973dd0a4..fcdb517c 100644
--- a/pkg/resources/stage_grant.go
+++ b/pkg/resources/stage_grant.go
@@ -7,7 +7,7 @@ import (
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 )
 
-var ValidStagePrivileges = NewPrivilegeSet(
+var validStagePrivileges = NewPrivilegeSet(
 	privilegeOwnership,
 	privilegeUsage,
 	// These privileges are only valid for internal stages
@@ -39,7 +39,7 @@ var stageGrantSchema = map[string]*schema.Schema{
 		Optional:     true,
 		Description:  "The privilege to grant on the stage.",
 		Default:      "USAGE",
-		ValidateFunc: validation.StringInSlice(ValidStagePrivileges.toList(), true),
+		ValidateFunc: validation.StringInSlice(validStagePrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -66,16 +66,19 @@ var stageGrantSchema = map[string]*schema.Schema{
 }
 
 // StageGrant returns a pointer to the resource representing a stage grant
-func StageGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateStageGrant,
-		Read:   ReadStageGrant,
-		Delete: DeleteStageGrant,
-
-		Schema: stageGrantSchema,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+func StageGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateStageGrant,
+			Read:   ReadStageGrant,
+			Delete: DeleteStageGrant,
+
+			Schema: stageGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validStagePrivileges,
 	}
 }
 
@@ -148,7 +151,7 @@ func ReadStageGrant(d *schema.ResourceData, meta interface{}) error {
 	}
 
 	builder := snowflake.StageGrant(dbName, schemaName, stageName)
-	return readGenericGrant(d, meta, stageGrantSchema, builder, false, ValidStagePrivileges)
+	return readGenericGrant(d, meta, stageGrantSchema, builder, false, validStagePrivileges)
 }
 
 // DeleteStageGrant implements schema.DeleteFunc
diff --git a/pkg/resources/table_grant.go b/pkg/resources/table_grant.go
index cc52f375..13882702 100644
--- a/pkg/resources/table_grant.go
+++ b/pkg/resources/table_grant.go
@@ -40,8 +40,8 @@ var tableGrantSchema = map[string]*schema.Schema{
 		Type:         schema.TypeString,
 		Optional:     true,
 		Description:  "The privilege to grant on the current or future table.",
-		Default:      "SELECT",
-		ValidateFunc: validation.StringInSlice(validTablePrivileges.toList(), true),
+		Default:      privilegeSelect.String(),
+		ValidateFunc: validation.StringInSlice(validTablePrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -76,16 +76,19 @@ var tableGrantSchema = map[string]*schema.Schema{
 }
 
 // TableGrant returns a pointer to the resource representing a Table grant
-func TableGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateTableGrant,
-		Read:   ReadTableGrant,
-		Delete: DeleteTableGrant,
-
-		Schema: tableGrantSchema,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+func TableGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateTableGrant,
+			Read:   ReadTableGrant,
+			Delete: DeleteTableGrant,
+
+			Schema: tableGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validTablePrivileges,
 	}
 }
 
diff --git a/pkg/resources/view_grant.go b/pkg/resources/view_grant.go
index adb57e55..504dd6f2 100644
--- a/pkg/resources/view_grant.go
+++ b/pkg/resources/view_grant.go
@@ -8,7 +8,7 @@ import (
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 )
 
-var ValidViewPrivileges = NewPrivilegeSet(
+var validViewPrivileges = NewPrivilegeSet(
 	privilegeSelect,
 )
 
@@ -35,8 +35,8 @@ var viewGrantSchema = map[string]*schema.Schema{
 		Type:         schema.TypeString,
 		Optional:     true,
 		Description:  "The privilege to grant on the current or future view.",
-		Default:      "SELECT",
-		ValidateFunc: validation.StringInSlice(ValidViewPrivileges.toList(), true),
+		Default:      privilegeSelect.String(),
+		ValidateFunc: validation.StringInSlice(validViewPrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -71,16 +71,19 @@ var viewGrantSchema = map[string]*schema.Schema{
 }
 
 // ViewGrant returns a pointer to the resource representing a view grant
-func ViewGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateViewGrant,
-		Read:   ReadViewGrant,
-		Delete: DeleteViewGrant,
-
-		Schema: viewGrantSchema,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+func ViewGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateViewGrant,
+			Read:   ReadViewGrant,
+			Delete: DeleteViewGrant,
+
+			Schema: viewGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validViewPrivileges,
 	}
 }
 
@@ -191,7 +194,7 @@ func ReadViewGrant(d *schema.ResourceData, meta interface{}) error {
 		builder = snowflake.ViewGrant(dbName, schemaName, viewName)
 	}
 
-	return readGenericGrant(d, meta, viewGrantSchema, builder, futureViewsEnabled, ValidViewPrivileges)
+	return readGenericGrant(d, meta, viewGrantSchema, builder, futureViewsEnabled, validViewPrivileges)
 }
 
 // DeleteViewGrant implements schema.DeleteFunc
diff --git a/pkg/resources/warehouse_grant.go b/pkg/resources/warehouse_grant.go
index dba5957f..721ec934 100644
--- a/pkg/resources/warehouse_grant.go
+++ b/pkg/resources/warehouse_grant.go
@@ -24,8 +24,8 @@ var warehouseGrantSchema = map[string]*schema.Schema{
 		Type:         schema.TypeString,
 		Optional:     true,
 		Description:  "The privilege to grant on the warehouse.",
-		Default:      "USAGE",
-		ValidateFunc: validation.StringInSlice(validWarehousePrivileges.toList(), true),
+		Default:      privilegeUsage.String(),
+		ValidateFunc: validation.StringInSlice(validWarehousePrivileges.ToList(), true),
 		ForceNew:     true,
 	},
 	"roles": {
@@ -45,17 +45,20 @@ var warehouseGrantSchema = map[string]*schema.Schema{
 }
 
 // WarehouseGrant returns a pointer to the resource representing a warehouse grant
-func WarehouseGrant() *schema.Resource {
-	return &schema.Resource{
-		Create: CreateWarehouseGrant,
-		Read:   ReadWarehouseGrant,
-		Delete: DeleteWarehouseGrant,
+func WarehouseGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateWarehouseGrant,
+			Read:   ReadWarehouseGrant,
+			Delete: DeleteWarehouseGrant,
 
-		Schema: warehouseGrantSchema,
-		// FIXME - tests for this don't currently work
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
+			Schema: warehouseGrantSchema,
+			// FIXME - tests for this don't currently work
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
 		},
+		ValidPrivs: validWarehousePrivileges,
 	}
 }
 


Test patch:
diff --git a/pkg/resources/account_grant_test.go b/pkg/resources/account_grant_test.go
index 6d63bb77..9931283b 100644
--- a/pkg/resources/account_grant_test.go
+++ b/pkg/resources/account_grant_test.go
@@ -16,7 +16,7 @@ import (
 //lintignore:AT003
 func TestAccountGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.AccountGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.AccountGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -29,7 +29,7 @@ func TestAccountGrantCreate(t *testing.T) { //lintignore:AT003
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.AccountGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.AccountGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/database_grant_test.go b/pkg/resources/database_grant_test.go
index 4bad5e55..f5316514 100644
--- a/pkg/resources/database_grant_test.go
+++ b/pkg/resources/database_grant_test.go
@@ -17,7 +17,7 @@ import (
 
 func TestDatabaseGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.DatabaseGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.DatabaseGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -31,7 +31,7 @@ func TestDatabaseGrantCreate(t *testing.T) {
 		"shares":            []interface{}{"test-share-1", "test-share-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.DatabaseGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.DatabaseGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go
index 9fd496c1..fbf58375 100644
--- a/pkg/resources/helpers_test.go
+++ b/pkg/resources/helpers_test.go
@@ -19,7 +19,7 @@ func database(t *testing.T, id string, params map[string]interface{}) *schema.Re
 
 func databaseGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.DatabaseGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.DatabaseGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -27,7 +27,7 @@ func databaseGrant(t *testing.T, id string, params map[string]interface{}) *sche
 
 func schemaGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -35,7 +35,7 @@ func schemaGrant(t *testing.T, id string, params map[string]interface{}) *schema
 
 func stageGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.StageGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.StageGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -43,7 +43,7 @@ func stageGrant(t *testing.T, id string, params map[string]interface{}) *schema.
 
 func tableGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.TableGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.TableGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -51,7 +51,7 @@ func tableGrant(t *testing.T, id string, params map[string]interface{}) *schema.
 
 func viewGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -59,7 +59,7 @@ func viewGrant(t *testing.T, id string, params map[string]interface{}) *schema.R
 
 func resourceMonitorGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.ResourceMonitorGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.ResourceMonitorGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -67,7 +67,7 @@ func resourceMonitorGrant(t *testing.T, id string, params map[string]interface{}
 
 func integrationGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.IntegrationGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.IntegrationGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
@@ -75,7 +75,7 @@ func integrationGrant(t *testing.T, id string, params map[string]interface{}) *s
 
 func accountGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
 	r := require.New(t)
-	d := schema.TestResourceDataRaw(t, resources.AccountGrant().Schema, params)
+	d := schema.TestResourceDataRaw(t, resources.AccountGrant().Resource.Schema, params)
 	r.NotNil(d)
 	d.SetId(id)
 	return d
diff --git a/pkg/resources/integration_grant_test.go b/pkg/resources/integration_grant_test.go
index 99529276..2e74f29c 100644
--- a/pkg/resources/integration_grant_test.go
+++ b/pkg/resources/integration_grant_test.go
@@ -17,7 +17,7 @@ import (
 
 func TestIntegrationGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.IntegrationGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.IntegrationGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -30,7 +30,7 @@ func TestIntegrationGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.IntegrationGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.IntegrationGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/resource_monitor_grant_test.go b/pkg/resources/resource_monitor_grant_test.go
index d5c654c6..b6096f0e 100644
--- a/pkg/resources/resource_monitor_grant_test.go
+++ b/pkg/resources/resource_monitor_grant_test.go
@@ -17,7 +17,7 @@ import (
 
 func TestResourceMonitorGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.ResourceMonitorGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.ResourceMonitorGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -30,7 +30,7 @@ func TestResourceMonitorGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.ResourceMonitorGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.ResourceMonitorGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/schema_grant_test.go b/pkg/resources/schema_grant_test.go
index 44bea641..f4db690e 100644
--- a/pkg/resources/schema_grant_test.go
+++ b/pkg/resources/schema_grant_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestSchemaGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.SchemaGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.SchemaGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -32,7 +32,7 @@ func TestSchemaGrantCreate(t *testing.T) {
 			"shares":            []interface{}{"test-share-1", "test-share-2"},
 			"with_grant_option": true,
 		}
-		d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Schema, in)
+		d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Resource.Schema, in)
 		r.NotNil(d)
 
 		WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
@@ -110,7 +110,7 @@ func TestFutureSchemaGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.SchemaGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/stage_grant_test.go b/pkg/resources/stage_grant_test.go
index 37efe015..21015f8f 100644
--- a/pkg/resources/stage_grant_test.go
+++ b/pkg/resources/stage_grant_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestStageGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.StageGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.StageGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -33,7 +33,7 @@ func TestStageGrantCreate(t *testing.T) {
 			"shares":            []interface{}{"test-share-1", "test-share-2"},
 			"with_grant_option": true,
 		}
-		d := schema.TestResourceDataRaw(t, resources.StageGrant().Schema, in)
+		d := schema.TestResourceDataRaw(t, resources.StageGrant().Resource.Schema, in)
 		r.NotNil(d)
 
 		WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/table_grant_test.go b/pkg/resources/table_grant_test.go
index 0ec21280..39af8a7b 100644
--- a/pkg/resources/table_grant_test.go
+++ b/pkg/resources/table_grant_test.go
@@ -17,7 +17,7 @@ import (
 
 func TestTableGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.TableGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.TableGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -33,7 +33,7 @@ func TestTableGrantCreate(t *testing.T) {
 		"shares":            []interface{}{"test-share-1", "test-share-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.TableGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.TableGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
@@ -104,7 +104,7 @@ func TestFutureTableGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.TableGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.TableGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
@@ -135,7 +135,7 @@ func TestFutureTableGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": false,
 	}
-	d = schema.TestResourceDataRaw(t, resources.TableGrant().Schema, in)
+	d = schema.TestResourceDataRaw(t, resources.TableGrant().Resource.Schema, in)
 	b.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/view_grant_test.go b/pkg/resources/view_grant_test.go
index b53e96d8..7f35e792 100644
--- a/pkg/resources/view_grant_test.go
+++ b/pkg/resources/view_grant_test.go
@@ -15,7 +15,7 @@ import (
 
 func TestViewGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.ViewGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.ViewGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -31,7 +31,7 @@ func TestViewGrantCreate(t *testing.T) {
 		"shares":            []interface{}{"test-share-1", "test-share-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
@@ -103,7 +103,7 @@ func TestFutureViewGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": true,
 	}
-	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.ViewGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
@@ -135,7 +135,7 @@ func TestFutureViewGrantCreate(t *testing.T) {
 		"roles":             []interface{}{"test-role-1", "test-role-2"},
 		"with_grant_option": false,
 	}
-	d = schema.TestResourceDataRaw(t, resources.ViewGrant().Schema, in)
+	d = schema.TestResourceDataRaw(t, resources.ViewGrant().Resource.Schema, in)
 	b.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
diff --git a/pkg/resources/warehouse_grant_test.go b/pkg/resources/warehouse_grant_test.go
index fa6b866b..fc7a7c4a 100644
--- a/pkg/resources/warehouse_grant_test.go
+++ b/pkg/resources/warehouse_grant_test.go
@@ -6,18 +6,16 @@ import (
 	"time"
 
 	sqlmock "github.com/DATA-DOG/go-sqlmock"
-	"github.com/stretchr/testify/require"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider"
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources"
 	. "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/stretchr/testify/require"
 )
 
 func TestWarehouseGrant(t *testing.T) {
 	r := require.New(t)
-	err := resources.WarehouseGrant().InternalValidate(provider.Provider().Schema, true)
+	err := resources.WarehouseGrant().Resource.InternalValidate(provider.Provider().Schema, true)
 	r.NoError(err)
 }
 
@@ -29,7 +27,7 @@ func TestWarehouseGrantCreate(t *testing.T) {
 		"privilege":      "USAGE",
 		"roles":          []interface{}{"test-role-1", "test-role-2"},
 	}
-	d := schema.TestResourceDataRaw(t, resources.WarehouseGrant().Schema, in)
+	d := schema.TestResourceDataRaw(t, resources.WarehouseGrant().Resource.Schema, in)
 	r.NotNil(d)
 
 	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {