Repository: chanzuckerberg/terraform-provider-snowflake (go)

Issue:
Support for `ROW ACCESS POLICY`
**Describe the solution you'd like**

Support for for the `ROW ACCESS POLICY` resource

**Additional context**

https://docs.snowflake.com/en/sql-reference/sql/create-row-access-policy.html

https://docs.snowflake.com/en/user-guide/security-row-using.html

Data access management is one of the things that should definitely be done via IaC :)

Pull request description:
feat: Added Row Access Policy Resources
Implemented Row Access Policy:

- Row Access Policies Datasource
- Row Access Policy Resource
- Row Access Policy Grant Resource

Resolves #615

## Test Plan
* [x] acceptance tests
* [x] unit tests

## References
* https://docs.snowflake.com/en/user-guide/security-row.html


Patch:
diff --git a/examples/data-sources/snowflake_row_access_policies/data-source.tf b/examples/data-sources/snowflake_row_access_policies/data-source.tf
new file mode 100644
index 00000000..4ba5df62
--- /dev/null
+++ b/examples/data-sources/snowflake_row_access_policies/data-source.tf
@@ -0,0 +1,4 @@
+data "snowflake_row_access_policies" "current" {
+    database = "MYDB"
+    schema   = "MYSCHEMA"
+}
\ No newline at end of file
diff --git a/examples/resources/snowflake_row_access_policy/import.sh b/examples/resources/snowflake_row_access_policy/import.sh
new file mode 100644
index 00000000..e314b353
--- /dev/null
+++ b/examples/resources/snowflake_row_access_policy/import.sh
@@ -0,0 +1,2 @@
+# format is database name | schema name | policy name
+terraform import snowflake_row_access_policy.example 'dbName|schemaName|policyName'
diff --git a/examples/resources/snowflake_row_access_policy/resource.tf b/examples/resources/snowflake_row_access_policy/resource.tf
new file mode 100644
index 00000000..b89ff2be
--- /dev/null
+++ b/examples/resources/snowflake_row_access_policy/resource.tf
@@ -0,0 +1,10 @@
+resource "snowflake_row_access_policy" "example_row_access_policy" {
+  name               = "EXAMPLE_ROW_ACCESS_POLICY"
+  database           = "EXAMPLE_DB"
+  schema             = "EXAMPLE_SCHEMA"
+  signature          = {
+    A = "VARCHAR",
+    B = "VARCHAR"
+  }
+  row_access_expression = "case when current_role() in ('ANALYST') then true else false end"
+}
diff --git a/examples/resources/snowflake_row_access_policy_grant/import.sh b/examples/resources/snowflake_row_access_policy_grant/import.sh
new file mode 100644
index 00000000..a77e4e72
--- /dev/null
+++ b/examples/resources/snowflake_row_access_policy_grant/import.sh
@@ -0,0 +1,2 @@
+# format is database name | schema name | row access policy name | privilege | true/false for with_grant_option
+terraform import snowflake_row_access_policy_grant.example 'dbName|schemaName|rowAccessPolicyName|SELECT|false'
diff --git a/examples/resources/snowflake_row_access_policy_grant/resource.tf b/examples/resources/snowflake_row_access_policy_grant/resource.tf
new file mode 100644
index 00000000..0c2edc8c
--- /dev/null
+++ b/examples/resources/snowflake_row_access_policy_grant/resource.tf
@@ -0,0 +1,13 @@
+resource "snowflake_row_access_policy_grant" "grant" {
+  database_name          = "db"
+  schema_name            = "schema"
+  row_access_policy_name = "row_access_policy"
+
+  privilege = "APPLY"
+  roles = [
+    "role1",
+    "role2",
+  ]
+
+  with_grant_option = false
+}
diff --git a/pkg/datasources/row_access_policies.go b/pkg/datasources/row_access_policies.go
new file mode 100644
index 00000000..8019551d
--- /dev/null
+++ b/pkg/datasources/row_access_policies.go
@@ -0,0 +1,90 @@
+package datasources
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+var rowAccessPoliciesSchema = map[string]*schema.Schema{
+	"database": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The database from which to return the schemas from.",
+	},
+	"schema": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The schema from which to return the row access policyfrom.",
+	},
+	"row_access_policies": {
+		Type:        schema.TypeList,
+		Computed:    true,
+		Description: "The row access policy in the schema",
+		Elem: &schema.Resource{
+			Schema: map[string]*schema.Schema{
+				"name": {
+					Type:     schema.TypeString,
+					Computed: true,
+				},
+				"database": {
+					Type:     schema.TypeString,
+					Computed: true,
+				},
+				"schema": {
+					Type:     schema.TypeString,
+					Computed: true,
+				},
+				"comment": {
+					Type:     schema.TypeString,
+					Optional: true,
+					Computed: true,
+				},
+			},
+		},
+	},
+}
+
+func RowAccessPolicies() *schema.Resource {
+	return &schema.Resource{
+		Read:   ReadRowAccessPolicies,
+		Schema: rowAccessPoliciesSchema,
+	}
+}
+
+func ReadRowAccessPolicies(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	databaseName := d.Get("database").(string)
+	schemaName := d.Get("schema").(string)
+
+	currentRowAccessPolicies, err := snowflake.ListRowAccessPolicies(databaseName, schemaName, db)
+	if err == sql.ErrNoRows {
+		// If not found, mark resource to be removed from statefile during apply or refresh
+		log.Printf("[DEBUG] row access policy in schema (%s) not found", d.Id())
+		d.SetId("")
+		return nil
+	} else if err != nil {
+		log.Printf("[DEBUG] unable to parse row access policy in schema (%s)", d.Id())
+		d.SetId("")
+		return nil
+	}
+
+	rowAccessPolicies := []map[string]interface{}{}
+
+	for _, rowAccessPolicy := range currentRowAccessPolicies {
+		rowAccessPolicyMap := map[string]interface{}{}
+
+		rowAccessPolicyMap["name"] = rowAccessPolicy.Name.String
+		rowAccessPolicyMap["database"] = rowAccessPolicy.DatabaseName.String
+		rowAccessPolicyMap["schema"] = rowAccessPolicy.SchemaName.String
+		rowAccessPolicyMap["comment"] = rowAccessPolicy.Comment.String
+
+		rowAccessPolicies = append(rowAccessPolicies, rowAccessPolicyMap)
+	}
+
+	d.SetId(fmt.Sprintf(`%v|%v`, databaseName, schemaName))
+	return d.Set("row_access_policies", rowAccessPolicies)
+}
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 442a5000..beba2b5e 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -141,6 +141,7 @@ func GetGrantResources() resources.TerraformGrantResources {
 		"snowflake_pipe_grant":              resources.PipeGrant(),
 		"snowflake_procedure_grant":         resources.ProcedureGrant(),
 		"snowflake_resource_monitor_grant":  resources.ResourceMonitorGrant(),
+		"snowflake_row_access_policy_grant": resources.RowAccessPolicyGrant(),
 		"snowflake_schema_grant":            resources.SchemaGrant(),
 		"snowflake_sequence_grant":          resources.SequenceGrant(),
 		"snowflake_stage_grant":             resources.StageGrant(),
@@ -169,6 +170,7 @@ func getResources() map[string]*schema.Resource {
 		"snowflake_resource_monitor":          resources.ResourceMonitor(),
 		"snowflake_role":                      resources.Role(),
 		"snowflake_role_grants":               resources.RoleGrants(),
+		"snowflake_row_access_policy":         resources.RowAccessPolicy(),
 		"snowflake_schema":                    resources.Schema(),
 		"snowflake_scim_integration":          resources.SCIMIntegration(),
 		"snowflake_sequence":                  resources.Sequence(),
@@ -215,6 +217,7 @@ func getDataSources() map[string]*schema.Resource {
 		"snowflake_warehouses":                         datasources.Warehouses(),
 		"snowflake_resource_monitors":                  datasources.ResourceMonitors(),
 		"snowflake_storage_integrations":               datasources.StorageIntegrations(),
+		"snowflake_row_access_policies":                datasources.RowAccessPolicies(),
 	}
 
 	return dataSources
diff --git a/pkg/resources/row_access_policy.go b/pkg/resources/row_access_policy.go
new file mode 100644
index 00000000..4af5e2ef
--- /dev/null
+++ b/pkg/resources/row_access_policy.go
@@ -0,0 +1,314 @@
+package resources
+
+import (
+	"bytes"
+	"database/sql"
+	"encoding/csv"
+	"fmt"
+	"strings"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/pkg/errors"
+)
+
+const (
+	rowAccessPolicyIDDelimiter = '|'
+)
+
+var rowAccessPolicySchema = map[string]*schema.Schema{
+	"name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specifies the identifier for the row access policy; must be unique for the database and schema in which the row access policy is created.",
+		ForceNew:    true,
+	},
+	"database": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The database in which to create the row access policy.",
+		ForceNew:    true,
+	},
+	"schema": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The schema in which to create the row access policy.",
+		ForceNew:    true,
+	},
+	"signature": {
+		Type:        schema.TypeMap,
+		Elem:        &schema.Schema{Type: schema.TypeString},
+		Required:    true,
+		ForceNew:    true,
+		Description: "Specifies signature (arguments) for the row access policy (uppercase and sorted to avoid recreation of resource). A signature specifies a set of attributes that must be considered to determine whether the row is accessible. The attribute values come from the database object (e.g. table or view) to be protected by the row access policy.",
+		//Implement DiffSuppressFunc after https://github.com/hashicorp/terraform-plugin-sdk/issues/477 is solved
+	},
+	"row_access_expression": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "Specifies the SQL expression. The expression can be any boolean-valued SQL expression.",
+	},
+	"comment": {
+		Type:        schema.TypeString,
+		Optional:    true,
+		Description: "Specifies a comment for the row access policy.",
+	},
+}
+
+type rowAccessPolicyID struct {
+	DatabaseName        string
+	SchemaName          string
+	RowAccessPolicyName string
+}
+
+// String() takes in a rowAccessPolicyID object and returns a pipe-delimited string:
+// DatabaseName|SchemaName|RowAccessPolicyName
+func (rapi *rowAccessPolicyID) String() (string, error) {
+	var buf bytes.Buffer
+	csvWriter := csv.NewWriter(&buf)
+	csvWriter.Comma = rowAccessPolicyIDDelimiter
+	dataIdentifiers := [][]string{{rapi.DatabaseName, rapi.SchemaName, rapi.RowAccessPolicyName}}
+	err := csvWriter.WriteAll(dataIdentifiers)
+	if err != nil {
+		return "", err
+	}
+	strRowAccessPolicyID := strings.TrimSpace(buf.String())
+	return strRowAccessPolicyID, nil
+}
+
+/// rowAccessPolicyIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|RowAccessPolicyName
+// and returns a rowAccessPolicyID object
+func rowAccessPolicyIDFromString(stringID string) (*rowAccessPolicyID, error) {
+	reader := csv.NewReader(strings.NewReader(stringID))
+	reader.Comma = rowAccessPolicyIDDelimiter
+	lines, err := reader.ReadAll()
+	if err != nil {
+		return nil, fmt.Errorf("Not CSV compatible")
+	}
+
+	if len(lines) != 1 {
+		return nil, fmt.Errorf("1 line per row access policy")
+	}
+	if len(lines[0]) != 3 {
+		return nil, fmt.Errorf("3 fields allowed")
+	}
+
+	rowAccessPolicyResult := &rowAccessPolicyID{
+		DatabaseName:        lines[0][0],
+		SchemaName:          lines[0][1],
+		RowAccessPolicyName: lines[0][2],
+	}
+	return rowAccessPolicyResult, nil
+}
+
+// RowAccessPolicy returns a pointer to the resource representing a row access policy
+func RowAccessPolicy() *schema.Resource {
+	return &schema.Resource{
+		Create: CreateRowAccessPolicy,
+		Read:   ReadRowAccessPolicy,
+		Update: UpdateRowAccessPolicy,
+		Delete: DeleteRowAccessPolicy,
+
+		Schema: rowAccessPolicySchema,
+		Importer: &schema.ResourceImporter{
+			StateContext: schema.ImportStatePassthroughContext,
+		},
+	}
+}
+
+// CreateRowAccessPolicy implements schema.CreateFunc
+func CreateRowAccessPolicy(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	name := d.Get("name").(string)
+	database := d.Get("database").(string)
+	schema := d.Get("schema").(string)
+	signature := d.Get("signature").(map[string]interface{})
+	rowAccessExpression := d.Get("row_access_expression").(string)
+
+	builder := snowflake.RowAccessPolicy(name, database, schema)
+
+	builder.WithSignature(signature)
+	builder.WithRowAccessExpression(rowAccessExpression)
+
+	// Set optionals
+	if v, ok := d.GetOk("comment"); ok {
+		builder.WithComment(v.(string))
+	}
+
+	stmt := builder.Create()
+	err := snowflake.Exec(db, stmt)
+	if err != nil {
+		return errors.Wrapf(err, "error creating row access policy %v", name)
+	}
+
+	rowAccessPolicyID := &rowAccessPolicyID{
+		DatabaseName:        database,
+		SchemaName:          schema,
+		RowAccessPolicyName: name,
+	}
+	dataIDInput, err := rowAccessPolicyID.String()
+	if err != nil {
+		return err
+	}
+	d.SetId(dataIDInput)
+
+	return ReadRowAccessPolicy(d, meta)
+}
+
+// ReadRowAccessPolicy implements schema.ReadFunc
+func ReadRowAccessPolicy(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	rowAccessPolicyID, err := rowAccessPolicyIDFromString(d.Id())
+	if err != nil {
+		return err
+	}
+
+	dbName := rowAccessPolicyID.DatabaseName
+	schema := rowAccessPolicyID.SchemaName
+	policyName := rowAccessPolicyID.RowAccessPolicyName
+
+	builder := snowflake.RowAccessPolicy(policyName, dbName, schema)
+
+	showSQL := builder.Show()
+
+	row := snowflake.QueryRow(db, showSQL)
+
+	s, err := snowflake.ScanRowAccessPolicies(row)
+	if err != nil {
+		return err
+	}
+
+	err = d.Set("name", s.Name.String)
+	if err != nil {
+		return err
+	}
+
+	err = d.Set("database", s.DatabaseName.String)
+	if err != nil {
+		return err
+	}
+
+	err = d.Set("schema", s.SchemaName.String)
+	if err != nil {
+		return err
+	}
+
+	err = d.Set("comment", s.Comment.String)
+	if err != nil {
+		return err
+	}
+
+	descSQL := builder.Describe()
+	rows, err := snowflake.Query(db, descSQL)
+	if err != nil {
+		return err
+	}
+
+	var (
+		name       string
+		signature  string
+		returnType string
+		body       string
+	)
+	for rows.Next() {
+		err := rows.Scan(&name, &signature, &returnType, &body)
+		if err != nil {
+			return err
+		}
+
+		err = d.Set("row_access_expression", body)
+		if err != nil {
+			return err
+		}
+
+		err = d.Set("signature", ParseSignature(signature))
+		if err != nil {
+			return err
+		}
+	}
+
+	return err
+}
+
+// UpdateRowAccessPolicy implements schema.UpdateFunc
+func UpdateRowAccessPolicy(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+
+	rowAccessPolicyID, err := rowAccessPolicyIDFromString(d.Id())
+	if err != nil {
+		return err
+	}
+
+	dbName := rowAccessPolicyID.DatabaseName
+	schema := rowAccessPolicyID.SchemaName
+	policyName := rowAccessPolicyID.RowAccessPolicyName
+
+	builder := snowflake.RowAccessPolicy(policyName, dbName, schema)
+
+	if d.HasChange("comment") {
+		comment := d.Get("comment")
+		if c := comment.(string); c == "" {
+			q := builder.RemoveComment()
+			err := snowflake.Exec(db, q)
+			if err != nil {
+				return errors.Wrapf(err, "error unsetting comment for row access policy on %v", d.Id())
+			}
+		} else {
+			q := builder.ChangeComment(c)
+			err := snowflake.Exec(db, q)
+			if err != nil {
+				return errors.Wrapf(err, "error updating comment for row access policy on %v", d.Id())
+			}
+		}
+	}
+
+	if d.HasChange("row_access_expression") {
+		rowAccessExpression := d.Get("row_access_expression")
+		q := builder.ChangeRowAccessExpression(rowAccessExpression.(string))
+		err := snowflake.Exec(db, q)
+		if err != nil {
+			return errors.Wrapf(err, "error updating row access policy expression on %v", d.Id())
+		}
+	}
+
+	return ReadRowAccessPolicy(d, meta)
+}
+
+// DeleteRowAccessPolicy implements schema.DeleteFunc
+func DeleteRowAccessPolicy(d *schema.ResourceData, meta interface{}) error {
+	db := meta.(*sql.DB)
+	rowAccessPolicyID, err := rowAccessPolicyIDFromString(d.Id())
+	if err != nil {
+		return err
+	}
+
+	dbName := rowAccessPolicyID.DatabaseName
+	schema := rowAccessPolicyID.SchemaName
+	policyName := rowAccessPolicyID.RowAccessPolicyName
+
+	q := snowflake.RowAccessPolicy(policyName, dbName, schema).Drop()
+
+	err = snowflake.Exec(db, q)
+	if err != nil {
+		return errors.Wrapf(err, "error deleting row access policy %v", d.Id())
+	}
+
+	d.SetId("")
+
+	return nil
+}
+
+func ParseSignature(signature string) map[string]interface{} {
+	// Format in database is `(column <data_type>)`
+	plainSignature := strings.ReplaceAll(signature, "(", "")
+	plainSignature = strings.ReplaceAll(plainSignature, ")", "")
+	signatureParts := strings.Split(plainSignature, ", ")
+	signatureMap := map[string]interface{}{}
+
+	for _, e := range signatureParts {
+		parts := strings.Split(e, " ")
+		signatureMap[parts[0]] = parts[1]
+	}
+
+	return signatureMap
+}
diff --git a/pkg/resources/row_access_policy_grant.go b/pkg/resources/row_access_policy_grant.go
new file mode 100644
index 00000000..f708911c
--- /dev/null
+++ b/pkg/resources/row_access_policy_grant.go
@@ -0,0 +1,158 @@
+package resources
+
+import (
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/validation"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+var validRowAccessPoilcyPrivileges = NewPrivilegeSet(
+	privilegeOwnership,
+	privilegeApply,
+)
+
+var rowAccessPolicyGrantSchema = map[string]*schema.Schema{
+	"database_name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The name of the database containing the row access policy on which to grant privileges.",
+		ForceNew:    true,
+	},
+	"row_access_policy_name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The name of the row access policy on which to grant privileges immediately.",
+		ForceNew:    true,
+	},
+	"privilege": {
+		Type:         schema.TypeString,
+		Optional:     true,
+		Description:  "The privilege to grant on the row access policy.",
+		Default:      "APPLY",
+		ValidateFunc: validation.ValidatePrivilege(validRowAccessPoilcyPrivileges.ToList(), true),
+		ForceNew:     true,
+	},
+	"roles": {
+		Type:        schema.TypeSet,
+		Elem:        &schema.Schema{Type: schema.TypeString},
+		Optional:    true,
+		Description: "Grants privilege to these roles.",
+		ForceNew:    true,
+	},
+	"schema_name": {
+		Type:        schema.TypeString,
+		Required:    true,
+		Description: "The name of the schema containing the row access policy on which to grant privileges.",
+		ForceNew:    true,
+	},
+	"with_grant_option": {
+		Type:        schema.TypeBool,
+		Optional:    true,
+		Description: "When this is set to true, allows the recipient role to grant the privileges to other roles.",
+		Default:     false,
+		ForceNew:    true,
+	},
+}
+
+// RowAccessPolicyGrant returns a pointer to the resource representing a row access policy grant
+func RowAccessPolicyGrant() *TerraformGrantResource {
+	return &TerraformGrantResource{
+		Resource: &schema.Resource{
+			Create: CreateRowAccessPolicyGrant,
+			Read:   ReadRowAccessPolicyGrant,
+			Delete: DeleteRowAccessPolicyGrant,
+
+			Schema: rowAccessPolicyGrantSchema,
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
+		},
+		ValidPrivs: validRowAccessPoilcyPrivileges,
+	}
+}
+
+// CreateRowAccessPolicyGrant implements schema.CreateFunc
+func CreateRowAccessPolicyGrant(d *schema.ResourceData, meta interface{}) error {
+	var rowAccessPolicyName string
+	if name, ok := d.GetOk("row_access_policy_name"); ok {
+		rowAccessPolicyName = name.(string)
+	}
+	dbName := d.Get("database_name").(string)
+	schemaName := d.Get("schema_name").(string)
+	priv := d.Get("privilege").(string)
+	grantOption := d.Get("with_grant_option").(bool)
+
+	builder := snowflake.RowAccessPolicyGrant(dbName, schemaName, rowAccessPolicyName)
+
+	err := createGenericGrant(d, meta, builder)
+	if err != nil {
+		return err
+	}
+
+	grant := &grantID{
+		ResourceName: dbName,
+		SchemaName:   schemaName,
+		ObjectName:   rowAccessPolicyName,
+		Privilege:    priv,
+		GrantOption:  grantOption,
+	}
+	dataIDInput, err := grant.String()
+	if err != nil {
+		return err
+	}
+	d.SetId(dataIDInput)
+
+	return ReadRowAccessPolicyGrant(d, meta)
+}
+
+// ReadRowAccessPolicyGrant implements schema.ReadFunc
+func ReadRowAccessPolicyGrant(d *schema.ResourceData, meta interface{}) error {
+	grantID, err := grantIDFromString(d.Id())
+	if err != nil {
+		return err
+	}
+	dbName := grantID.ResourceName
+	schemaName := grantID.SchemaName
+	rowAccessPolicyName := grantID.ObjectName
+	priv := grantID.Privilege
+
+	err = d.Set("database_name", dbName)
+	if err != nil {
+		return err
+	}
+	err = d.Set("schema_name", schemaName)
+	if err != nil {
+		return err
+	}
+	err = d.Set("row_access_policy_name", rowAccessPolicyName)
+	if err != nil {
+		return err
+	}
+	err = d.Set("privilege", priv)
+	if err != nil {
+		return err
+	}
+	err = d.Set("with_grant_option", grantID.GrantOption)
+	if err != nil {
+		return err
+	}
+
+	builder := snowflake.RowAccessPolicyGrant(dbName, schemaName, rowAccessPolicyName)
+
+	return readGenericGrant(d, meta, rowAccessPolicyGrantSchema, builder, false, validRowAccessPoilcyPrivileges)
+}
+
+// DeleteRowAccessPolicyGrant implements schema.DeleteFunc
+func DeleteRowAccessPolicyGrant(d *schema.ResourceData, meta interface{}) error {
+	grantID, err := grantIDFromString(d.Id())
+	if err != nil {
+		return err
+	}
+	dbName := grantID.ResourceName
+	schemaName := grantID.SchemaName
+	rowAccessPolicyName := grantID.ObjectName
+
+	builder := snowflake.RowAccessPolicyGrant(dbName, schemaName, rowAccessPolicyName)
+
+	return deleteGenericGrant(d, meta, builder)
+}
diff --git a/pkg/snowflake/grant.go b/pkg/snowflake/grant.go
index 86db10d1..2f100579 100644
--- a/pkg/snowflake/grant.go
+++ b/pkg/snowflake/grant.go
@@ -29,6 +29,7 @@ const (
 	maskingPolicyType    grantType = "MASKING POLICY"
 	pipeType             grantType = "PIPE"
 	taskType             grantType = "TASK"
+	rowAccessPolicyType  grantType = "ROW ACCESS POLICY"
 )
 
 type GrantExecutable interface {
@@ -282,6 +283,15 @@ func TaskGrant(db, schema, task string) GrantBuilder {
 	}
 }
 
+// RowAccessPolicyGrant returns a pointer to a CurrentGrantBuilder for a masking policy
+func RowAccessPolicyGrant(db, schema, rowAccessPolicy string) GrantBuilder {
+	return &CurrentGrantBuilder{
+		name:          rowAccessPolicy,
+		qualifiedName: fmt.Sprintf(`"%v"."%v"."%v"`, db, schema, rowAccessPolicy),
+		grantType:     rowAccessPolicyType,
+	}
+}
+
 type granteeType string
 
 const (
diff --git a/pkg/snowflake/row_access_policy.go b/pkg/snowflake/row_access_policy.go
new file mode 100644
index 00000000..30dada79
--- /dev/null
+++ b/pkg/snowflake/row_access_policy.go
@@ -0,0 +1,166 @@
+package snowflake
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/jmoiron/sqlx"
+	pe "github.com/pkg/errors"
+)
+
+// RowAccessPolicyBuilder abstracts the creation of SQL queries for a Snowflake Row Access Policy
+type RowAccessPolicyBuilder struct {
+	name                string
+	db                  string
+	schema              string
+	comment             string
+	signature           map[string]interface{}
+	rowAccessExpression string
+}
+
+// QualifiedName prepends the db and schema if set and escapes everything nicely
+func (rapb *RowAccessPolicyBuilder) QualifiedName() string {
+	var n strings.Builder
+
+	if rapb.db != "" && rapb.schema != "" {
+		n.WriteString(fmt.Sprintf(`"%v"."%v".`, rapb.db, rapb.schema))
+	}
+
+	if rapb.db != "" && rapb.schema == "" {
+		n.WriteString(fmt.Sprintf(`"%v"..`, rapb.db))
+	}
+
+	if rapb.db == "" && rapb.schema != "" {
+		n.WriteString(fmt.Sprintf(`"%v".`, rapb.schema))
+	}
+
+	n.WriteString(fmt.Sprintf(`"%v"`, rapb.name))
+
+	return n.String()
+}
+
+// WithComment adds a comment to the RowAccessPolicyBuilder
+func (rapb *RowAccessPolicyBuilder) WithComment(c string) *RowAccessPolicyBuilder {
+	rapb.comment = EscapeString(c)
+	return rapb
+}
+
+// WithSignature adds signature to the RowAccessPolicyBuilder
+func (rapb *RowAccessPolicyBuilder) WithSignature(signature map[string]interface{}) *RowAccessPolicyBuilder {
+	rapb.signature = signature
+	return rapb
+}
+
+// WithRowAccessExpression adds rowAccessExpression to the RowAccessPolicyBuilder
+func (rapb *RowAccessPolicyBuilder) WithRowAccessExpression(rowAccessExpression string) *RowAccessPolicyBuilder {
+	rapb.rowAccessExpression = rowAccessExpression
+	return rapb
+}
+
+// RowAccessPolicy returns a pointer to a Builder that abstracts the DDL operations for a row access policy.
+//
+// Supported DDL operations are:
+//   - CREATE ROW ACCESS POLICY
+//	 - ALTER ROW ACCESS POLICY
+//   - DROP ROW ACCESS POLICY
+//   - SHOW ROW ACCESS POLICIES
+//   - DESCRIBE ROW ACCESS POLICY
+//
+func RowAccessPolicy(name, db, schema string) *RowAccessPolicyBuilder {
+	return &RowAccessPolicyBuilder{
+		name:   name,
+		db:     db,
+		schema: schema,
+	}
+}
+
+// Create returns the SQL query that will create a row access policy.
+func (rapb *RowAccessPolicyBuilder) Create() string {
+	q := strings.Builder{}
+
+	sortedSignature := []string{}
+	for _, k := range sortInterfaceStrings(rapb.signature) {
+		sortedSignature = append(sortedSignature, EscapeString(fmt.Sprintf(`%v %v`, k, rapb.signature[k])))
+	}
+	parsedSignature := fmt.Sprintf(`%v`, strings.Join(sortedSignature, ", "))
+
+	q.WriteString(fmt.Sprintf(`CREATE ROW ACCESS POLICY %v AS (%v) RETURNS BOOLEAN -> `, rapb.QualifiedName(), parsedSignature))
+
+	q.WriteString(rapb.rowAccessExpression)
+
+	if rapb.comment != "" {
+		q.WriteString(fmt.Sprintf(` COMMENT = '%v'`, EscapeString(rapb.comment)))
+	}
+
+	return q.String()
+}
+
+// Describe returns the SQL query that will describe a row access policy
+func (rapb *RowAccessPolicyBuilder) Describe() string {
+	return fmt.Sprintf(`DESCRIBE ROW ACCESS POLICY %v`, rapb.QualifiedName())
+}
+
+// ChangeComment returns the SQL query that will update the comment on the row access policy.
+func (rapb *RowAccessPolicyBuilder) ChangeComment(c string) string {
+	return fmt.Sprintf(`ALTER ROW ACCESS POLICY %v SET COMMENT = '%v'`, rapb.QualifiedName(), EscapeString(c))
+}
+
+// RemoveComment returns the SQL query that will remove the comment on the row access policy.
+func (rapb *RowAccessPolicyBuilder) RemoveComment() string {
+	return fmt.Sprintf(`ALTER ROW ACCESS POLICY %v UNSET COMMENT`, rapb.QualifiedName())
+}
+
+// ChangeRowAccessExpression returns the SQL query that will update the row access expression on the row access policy.
+func (rapb *RowAccessPolicyBuilder) ChangeRowAccessExpression(rowAccessExpression string) string {
+	q := strings.Builder{}
+	q.WriteString(fmt.Sprintf(`ALTER ROW ACCESS POLICY %v SET BODY -> `, rapb.QualifiedName()))
+
+	q.WriteString(rowAccessExpression)
+
+	return q.String()
+}
+
+// Drop returns the SQL query that will drop a row access policy.
+func (rapb *RowAccessPolicyBuilder) Drop() string {
+	return fmt.Sprintf(`DROP ROW ACCESS POLICY %v`, rapb.QualifiedName())
+}
+
+// Show returns the SQL query that will show a row access policy.
+func (rapb *RowAccessPolicyBuilder) Show() string {
+	return fmt.Sprintf(`SHOW ROW ACCESS POLICIES LIKE '%v' IN SCHEMA "%v"."%v"`, rapb.name, rapb.db, rapb.schema)
+}
+
+type RowAccessPolicyStruct struct {
+	CreatedOn    sql.NullString `db:"created_on"`
+	Name         sql.NullString `db:"name"`
+	DatabaseName sql.NullString `db:"database_name"`
+	SchemaName   sql.NullString `db:"schema_name"`
+	Kind         sql.NullString `db:"kind"`
+	Owner        sql.NullString `db:"owner"`
+	Comment      sql.NullString `db:"comment"`
+}
+
+func ScanRowAccessPolicies(row *sqlx.Row) (*RowAccessPolicyStruct, error) {
+	m := &RowAccessPolicyStruct{}
+	err := row.StructScan(m)
+	return m, err
+}
+
+func ListRowAccessPolicies(databaseName string, schemaName string, db *sql.DB) ([]RowAccessPolicyStruct, error) {
+	stmt := fmt.Sprintf(`SHOW ROW ACCESS POLICIES IN SCHEMA "%s"."%v"`, databaseName, schemaName)
+	rows, err := Query(db, stmt)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	dbs := []RowAccessPolicyStruct{}
+	err = sqlx.StructScan(rows, &dbs)
+	if err == sql.ErrNoRows {
+		log.Printf("[DEBUG] no row access policies found")
+		return nil, nil
+	}
+	return dbs, pe.Wrapf(err, "unable to scan row for %s", stmt)
+}
diff --git a/pkg/snowflake/sorting.go b/pkg/snowflake/sorting.go
index cd4babfe..b49f6223 100644
--- a/pkg/snowflake/sorting.go
+++ b/pkg/snowflake/sorting.go
@@ -4,6 +4,15 @@ import (
 	"sort"
 )
 
+func sortInterfaceStrings(strs map[string]interface{}) []string {
+	sortedStringProperties := []string{}
+	for k := range strs {
+		sortedStringProperties = append(sortedStringProperties, k)
+	}
+	sort.Strings(sortedStringProperties)
+	return sortedStringProperties
+}
+
 func sortStrings(strs map[string]string) []string {
 	sortedStringProperties := []string{}
 	for k := range strs {


Test patch:
diff --git a/pkg/datasources/row_access_policies_acceptance_test.go b/pkg/datasources/row_access_policies_acceptance_test.go
new file mode 100644
index 00000000..78bf5263
--- /dev/null
+++ b/pkg/datasources/row_access_policies_acceptance_test.go
@@ -0,0 +1,63 @@
+package datasources_test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAccRowAccessPolicies(t *testing.T) {
+	databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+	schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+	rowAccessPolicyName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+	resource.ParallelTest(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: rowAccessPolicies(databaseName, schemaName, rowAccessPolicyName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.snowflake_row_access_policies.v", "database", databaseName),
+					resource.TestCheckResourceAttr("data.snowflake_row_access_policies.v", "schema", schemaName),
+					resource.TestCheckResourceAttrSet("data.snowflake_row_access_policies.v", "row_access_policies.#"),
+					resource.TestCheckResourceAttr("data.snowflake_row_access_policies.v", "row_access_policies.#", "1"),
+					resource.TestCheckResourceAttr("data.snowflake_row_access_policies.v", "row_access_policies.0.name", rowAccessPolicyName),
+				),
+			},
+		},
+	})
+}
+
+func rowAccessPolicies(databaseName string, schemaName string, rowAccessPolicyName string) string {
+	return fmt.Sprintf(`
+
+	resource snowflake_database "test" {
+		name = "%v"
+	}
+
+	resource snowflake_schema "test"{
+		name 	 = "%v"
+		database = snowflake_database.test.name
+	}
+
+	resource "snowflake_row_access_policy" "test" {
+		name = "%v"
+		database = snowflake_database.test.name
+		schema = snowflake_schema.test.name
+		signature = {
+			N = "VARCHAR"
+			V = "VARCHAR",
+		}
+		row_access_expression = "case when current_role() in ('ANALYST') then true else false end"
+		comment = "Terraform acceptance test"
+	}
+
+	data snowflake_row_access_policies "v" {
+		database = snowflake_row_access_policy.test.database
+		schema = snowflake_row_access_policy.test.schema
+		depends_on = [snowflake_row_access_policy.test]
+	}
+	`, databaseName, schemaName, rowAccessPolicyName)
+}
diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go
index e3ccb45f..69a13c0c 100644
--- a/pkg/resources/helpers_test.go
+++ b/pkg/resources/helpers_test.go
@@ -350,3 +350,19 @@ func taskGrant(t *testing.T, id string, params map[string]interface{}) *schema.R
 	d.SetId(id)
 	return d
 }
+
+func rowAccessPolicy(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
+	r := require.New(t)
+	d := schema.TestResourceDataRaw(t, resources.RowAccessPolicy().Schema, params)
+	r.NotNil(d)
+	d.SetId(id)
+	return d
+}
+
+func rowAccessPolicyGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
+	r := require.New(t)
+	d := schema.TestResourceDataRaw(t, resources.RowAccessPolicyGrant().Resource.Schema, params)
+	r.NotNil(d)
+	d.SetId(id)
+	return d
+}
diff --git a/pkg/resources/row_access_policy_acceptance_test.go b/pkg/resources/row_access_policy_acceptance_test.go
new file mode 100644
index 00000000..80440fc4
--- /dev/null
+++ b/pkg/resources/row_access_policy_acceptance_test.go
@@ -0,0 +1,64 @@
+package resources_test
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAcc_RowAccessPolicy(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_ROW_ACCESS_POLICY_TESTS"); ok {
+		t.Skip("Skipping TestAccRowAccessPolicy")
+	}
+
+	accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+
+	resource.ParallelTest(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: rowAccessPolicyConfig(accName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "name", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "database", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "schema", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "comment", "Terraform acceptance test"),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "row_access_expression", "case when current_role() in ('ANALYST') then true else false end"),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.N", "VARCHAR"),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.V", "VARCHAR"),
+				),
+			},
+		},
+	})
+}
+
+func rowAccessPolicyConfig(n string) string {
+	return fmt.Sprintf(`
+resource "snowflake_database" "test" {
+	name = "%v"
+	comment = "Terraform acceptance test"
+}
+
+resource "snowflake_schema" "test" {
+	name = "%v"
+	database = snowflake_database.test.name
+	comment = "Terraform acceptance test"
+}
+
+resource "snowflake_row_access_policy" "test" {
+	name = "%v"
+	database = snowflake_database.test.name
+	schema = snowflake_schema.test.name
+	signature = {
+		N = "VARCHAR"
+		V = "VARCHAR",
+	}
+	row_access_expression = "case when current_role() in ('ANALYST') then true else false end"
+	comment = "Terraform acceptance test"
+}
+`, n, n, n)
+}
diff --git a/pkg/resources/row_access_policy_grant_acceptance_test.go b/pkg/resources/row_access_policy_grant_acceptance_test.go
new file mode 100644
index 00000000..a26ab032
--- /dev/null
+++ b/pkg/resources/row_access_policy_grant_acceptance_test.go
@@ -0,0 +1,74 @@
+package resources_test
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAcc_RowAccessPolicyGrant(t *testing.T) {
+	if _, ok := os.LookupEnv("SKIP_ROW_ACCESS_POLICY_TESTS"); ok {
+		t.Skip("Skipping TestAccRowAccessPolicy")
+	}
+
+	accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+
+	resource.ParallelTest(t, resource.TestCase{
+		Providers: providers(),
+		Steps: []resource.TestStep{
+			{
+				Config: rowAccessPolicyGrantConfig(accName),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("snowflake_row_access_policy_grant.test", "database_name", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy_grant.test", "schema_name", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy_grant.test", "row_access_policy_name", accName),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy_grant.test", "with_grant_option", "false"),
+					resource.TestCheckResourceAttr("snowflake_row_access_policy_grant.test", "privilege", "APPLY"),
+				),
+			},
+		},
+	})
+}
+
+func rowAccessPolicyGrantConfig(n string) string {
+	return fmt.Sprintf(`
+resource "snowflake_database" "test" {
+	name = "%v"
+	comment = "Terraform acceptance test"
+}
+
+resource "snowflake_schema" "test" {
+	name = "%v"
+	database = snowflake_database.test.name
+	comment = "Terraform acceptance test"
+}
+
+resource "snowflake_role" "test" {
+	name = "%v"
+}
+
+resource "snowflake_row_access_policy" "test" {
+	name = "%v"
+	database = snowflake_database.test.name
+	schema = snowflake_schema.test.name
+	signature = {
+		N = "VARCHAR"
+		V = "VARCHAR",
+	}
+	row_access_expression = "case when current_role() in ('ANALYST') then true else false end"
+	comment = "Terraform acceptance test"
+}
+
+resource "snowflake_row_access_policy_grant" "test" {
+	row_access_policy_name = snowflake_row_access_policy.test.name
+	database_name = snowflake_database.test.name
+	roles         = [snowflake_role.test.name]
+	schema_name   = snowflake_schema.test.name
+	privilege = "APPLY"
+}
+`, n, n, n, n)
+}
diff --git a/pkg/resources/row_access_policy_grant_test.go b/pkg/resources/row_access_policy_grant_test.go
new file mode 100644
index 00000000..88312256
--- /dev/null
+++ b/pkg/resources/row_access_policy_grant_test.go
@@ -0,0 +1,79 @@
+package resources_test
+
+import (
+	"database/sql"
+	"testing"
+	"time"
+
+	sqlmock "github.com/DATA-DOG/go-sqlmock"
+	"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 TestRowAccessPolicyGrant(t *testing.T) {
+	r := require.New(t)
+	err := resources.RowAccessPolicyGrant().Resource.InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestRowAccessPolicyGrantCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"row_access_policy_name": "test-row-access-policy",
+		"schema_name":            "PUBLIC",
+		"database_name":          "test-db",
+		"privilege":              "APPLY",
+		"roles":                  []interface{}{"test-role-1", "test-role-2"},
+	}
+	d := schema.TestResourceDataRaw(t, resources.RowAccessPolicyGrant().Resource.Schema, in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^GRANT APPLY ON ROW ACCESS POLICY "test-db"."PUBLIC"."test-row-access-policy" TO ROLE "test-role-1"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		mock.ExpectExec(`^GRANT APPLY ON ROW ACCESS POLICY "test-db"."PUBLIC"."test-row-access-policy" TO ROLE "test-role-2"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		expectReadRowAccessPolicyGrant(mock)
+		err := resources.CreateRowAccessPolicyGrant(d, db)
+		r.NoError(err)
+	})
+}
+
+func TestRowAccessPolicyGrantRead(t *testing.T) {
+	r := require.New(t)
+
+	d := rowAccessPolicyGrant(t, "test-db|PUBLIC|test-row-access-policy|APPLY|false", map[string]interface{}{
+		"row_access_policy_name": "test-row-access-policy",
+		"schema_name":            "PUBLIC",
+		"database_name":          "test-db",
+		"privilege":              "APPLY",
+		"roles":                  []interface{}{},
+		"with_grant_option":      false,
+	})
+
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		expectReadRowAccessPolicyGrant(mock)
+		err := resources.ReadRowAccessPolicyGrant(d, db)
+		r.NoError(err)
+	})
+
+	roles := d.Get("roles").(*schema.Set)
+	r.True(roles.Contains("test-role-1"))
+	r.True(roles.Contains("test-role-2"))
+	r.Equal(roles.Len(), 2)
+}
+
+func expectReadRowAccessPolicyGrant(mock sqlmock.Sqlmock) {
+	rows := sqlmock.NewRows([]string{
+		"created_on", "privilege", "granted_on", "name", "granted_to", "grantee_name", "grant_option", "granted_by",
+	}).AddRow(
+		time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "APPLY", "ROW_ACCESS_POLICY", "test-row-access-policy", "ROLE", "test-role-1", false, "bob",
+	).AddRow(
+		time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "APPLY", "ROW_ACCESS_POLICY", "test-row-access-policy", "ROLE", "test-role-2", false, "bob",
+	)
+	mock.ExpectQuery(`^SHOW GRANTS ON ROW ACCESS POLICY "test-db"."PUBLIC"."test-row-access-policy"$`).WillReturnRows(rows)
+}
diff --git a/pkg/resources/row_access_policy_test.go b/pkg/resources/row_access_policy_test.go
new file mode 100644
index 00000000..3b75213f
--- /dev/null
+++ b/pkg/resources/row_access_policy_test.go
@@ -0,0 +1,82 @@
+package resources_test
+
+import (
+	"database/sql"
+	"testing"
+	"time"
+
+	sqlmock "github.com/DATA-DOG/go-sqlmock"
+	"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/stretchr/testify/require"
+)
+
+func TestRowAccessPolicy(t *testing.T) {
+	r := require.New(t)
+	err := resources.RowAccessPolicy().InternalValidate(provider.Provider().Schema, true)
+	r.NoError(err)
+}
+
+func TestRowAccessPolicyCreate(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":                  "policy_name",
+		"database":              "database_name",
+		"schema":                "schema_name",
+		"comment":               "great comment",
+		"signature":             map[string]interface{}{"n": "VARCHAR", "v": "VARCHAR"},
+		"row_access_expression": "case when current_role() in ('ANALYST') then true else false end",
+	}
+
+	d := rowAccessPolicy(t, "database_name|schema_name|policy_name", in)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(
+			`^CREATE ROW ACCESS POLICY "database_name"."schema_name"."policy_name" AS \(n VARCHAR, v VARCHAR\) RETURNS BOOLEAN -> case when current_role\(\) in \('ANALYST'\) then true else false end COMMENT = \'great comment\'$`,
+		).WillReturnResult(sqlmock.NewResult(1, 1))
+		expectReadRowAccessPolicy(mock)
+		err := resources.CreateRowAccessPolicy(d, db)
+		r.NoError(err)
+		r.Equal("policy_name", d.Get("name").(string))
+	})
+}
+
+func expectReadRowAccessPolicy(mock sqlmock.Sqlmock) {
+	showRows := sqlmock.NewRows([]string{
+		"created_on", "name", "database_name", "schema_name", "kind", "owner", "comment",
+	}).AddRow(
+		time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "policy_name", "database_name", "schema_name", "ROW_ACCESS_POLICY", "test", "this is a comment",
+	)
+	mock.ExpectQuery(`^SHOW ROW ACCESS POLICIES LIKE 'policy_name' IN SCHEMA "database_name"."schema_name"$`).WillReturnRows(showRows)
+
+	descRows := sqlmock.NewRows([]string{
+		"name", "signature", "return_type", "body",
+	}).AddRow(
+		"policy_name", "(n VARCHAR, v VARCHAR)", "BOOLEAN", "case when current_role() in ('ANALYST') then val else sha2(val, 512) end",
+	)
+	mock.ExpectQuery(`^DESCRIBE ROW ACCESS POLICY "database_name"."schema_name"."policy_name"$`).WillReturnRows(descRows)
+}
+
+func TestRowAccessPolicyDelete(t *testing.T) {
+	r := require.New(t)
+
+	in := map[string]interface{}{
+		"name":                  "policy_name",
+		"database":              "database_name",
+		"schema":                "schema_name",
+		"comment":               "great comment",
+		"signature":             map[string]interface{}{"n": "VARCHAR", "v": "VARCHAR"},
+		"row_access_expression": "case when current_role() in ('ANALYST') then true else false end",
+	}
+
+	d := rowAccessPolicy(t, "database_name|schema_name|policy_name", in)
+	r.NotNil(d)
+
+	WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
+		mock.ExpectExec(`^DROP ROW ACCESS POLICY "database_name"."schema_name"."policy_name"$`).WillReturnResult(sqlmock.NewResult(1, 1))
+		err := resources.DeleteRowAccessPolicy(d, db)
+		r.NoError(err)
+	})
+}
diff --git a/pkg/snowflake/row_access_policy_test.go b/pkg/snowflake/row_access_policy_test.go
new file mode 100644
index 00000000..787b7f6c
--- /dev/null
+++ b/pkg/snowflake/row_access_policy_test.go
@@ -0,0 +1,79 @@
+package snowflake_test
+
+import (
+	"testing"
+
+	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRowAccessPolicyCreate(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	m.WithSignature(map[string]interface{}{"n": "string", "v": "string"})
+	m.WithRowAccessExpression(`case
+	when current_role() in ('ANALYST') then true
+	else false
+end`)
+	m.WithComment("This is a test comment")
+
+	q := m.Create()
+	r.Equal(`CREATE ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy" AS (n string, v string) RETURNS BOOLEAN -> case
+	when current_role() in ('ANALYST') then true
+	else false
+end COMMENT = 'This is a test comment'`, q)
+}
+
+func TestRowAccessPolicyDescribe(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	q := m.Describe()
+	r.Equal(`DESCRIBE ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy"`, q)
+}
+
+func TestRowAccessPolicyDrop(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	q := m.Drop()
+	r.Equal(`DROP ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy"`, q)
+}
+
+func TestRowAccessPolicyChangeComment(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	q := m.ChangeComment("test comment!")
+	r.Equal(`ALTER ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy" SET COMMENT = 'test comment!'`, q)
+}
+
+func TestRowAccessPolicyRemoveComment(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	q := m.RemoveComment()
+	r.Equal(`ALTER ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy" UNSET COMMENT`, q)
+}
+
+func TestRowAccessChangeRowAccessExpression(t *testing.T) {
+	r := require.New(t)
+	m := snowflake.RowAccessPolicy("test_row_access_policy", "test_db", "test_schema")
+	r.NotNil(m)
+
+	q := m.ChangeRowAccessExpression(`case
+	when current_role() in ('ANALYST') then true
+    else false
+end`)
+
+	r.Equal(`ALTER ROW ACCESS POLICY "test_db"."test_schema"."test_row_access_policy" SET BODY -> case
+	when current_role() in ('ANALYST') then true
+    else false
+end`, q)
+}
diff --git a/pkg/snowflake/sorting_test.go b/pkg/snowflake/sorting_test.go
index 92f40fdb..4f464167 100644
--- a/pkg/snowflake/sorting_test.go
+++ b/pkg/snowflake/sorting_test.go
@@ -11,6 +11,7 @@ func TestSortStrings(t *testing.T) {
 
 	ss := []string{"a", "b", "c"}
 
+	r.Equal(ss, sortInterfaceStrings(map[string]interface{}{"c": "", "b": "", "a": ""}))
 	r.Equal(ss, sortStrings(map[string]string{"c": "", "b": "", "a": ""}))
 	r.Equal(ss, sortStringList(map[string][]string{"c": {}, "b": {}, "a": {}}))
 	r.Equal(ss, sortStringsInt(map[string]int{"c": 0, "b": 1, "a": 2}))