Skip to content

Commit

Permalink
Fix empty USING in views masking policies
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jmichalak committed Nov 28, 2024
1 parent db30814 commit 8395c60
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 4 deletions.
21 changes: 21 additions & 0 deletions pkg/resources/diff_suppressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,24 @@ func IgnoreNewEmptyListOrSubfields(ignoredSubfields ...string) schema.SchemaDiff
return len(parts) == 3 && slices.Contains(ignoredSubfields, parts[2]) && new == ""
}
}

// IgnoreMatchingColumnNameAndMaskingPolicyUsingFirstElem ignores when the first element of USING is matching the column name.
// see USING section in https://docs.snowflake.com/en/sql-reference/sql/create-view#optional-parameters
func IgnoreMatchingColumnNameAndMaskingPolicyUsingFirstElem() schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
// suppress diff when the name of the column matches the name of using
parts := strings.SplitN(k, ".", 6)
if len(parts) < 6 {
log.Printf("[DEBUG] invalid resource key: %s", parts)
return false
}
// key is element count
if parts[5] == "#" && old == "1" && new == "0" {
return true
}
colNameKey := strings.Join([]string{parts[0], parts[1], "column_name"}, ".")
colName := d.Get(colNameKey).(string)

return new == "" && old == colName
}
}
83 changes: 83 additions & 0 deletions pkg/resources/diff_suppressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,86 @@ func Test_ignoreNewEmptyList(t *testing.T) {
})
}
}

func Test_IgnoreMatchingColumnNameAndMaskingPolicyUsingFirstElem(t *testing.T) {
resourceSchema := map[string]*schema.Schema{
"column": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"column_name": {
Type: schema.TypeString,
Required: true,
},
"masking_policy": {
Type: schema.TypeList,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"using": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
},
},
},
}
resourceData := func(using ...any) map[string]any {
return map[string]any{
"column": []any{
map[string]any{
"column_name": "foo",
"masking_policy": []any{
map[string]any{
"using": using,
},
},
},
},
}
}
tests := []struct {
name string
key string
old string
new string
resourceData *schema.ResourceData
wantSuppress bool
}{
// TODO: add more cases?
{
name: "suppress when USING is not specified in the config, but is in the state - check count",
key: "column.0.masking_policy.0.using.#",
old: "1",
new: "0",
resourceData: schema.TestResourceDataRaw(t, resourceSchema, resourceData("foo")),
wantSuppress: true,
},
{
name: "suppress when USING is not specified in the config, but is in the state - check elem",
key: "column.0.masking_policy.0.using.0",
old: "foo",
new: "",
resourceData: schema.TestResourceDataRaw(t, resourceSchema, resourceData("foo")),
wantSuppress: true,
},
{
name: "do not suppress when there is column name mismatch",
key: "column.0.masking_policy.0.using.0",
old: "foo",
new: "bar",
resourceData: schema.TestResourceDataRaw(t, resourceSchema, resourceData("foo")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.wantSuppress, resources.IgnoreMatchingColumnNameAndMaskingPolicyUsingFirstElem()(tt.key, tt.old, tt.new, tt.resourceData))
})
}
}
2 changes: 1 addition & 1 deletion pkg/resources/testdata/TestAcc_View/columns/test.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ resource "snowflake_view" "test" {

masking_policy {
policy_name = var.masking_name
using = var.masking_using
using = try(var.masking_using, null)
}
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/resources/testdata/TestAcc_View/columns/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ variable "masking_name" {
}

variable "masking_using" {
type = list(string)
type = list(string)
default = null
}
11 changes: 9 additions & 2 deletions pkg/resources/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ var viewSchema = map[string]*schema.Schema{
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "Specifies the arguments to pass into the conditional masking policy SQL expression. The first column in the list specifies the column for the policy conditions to mask or tokenize the data and must match the column to which the masking policy is set. The additional columns specify the columns to evaluate to determine whether to mask or tokenize the data in each row of the query result when a query is made on the first column. If the USING clause is omitted, Snowflake treats the conditional masking policy as a normal masking policy.",
DiffSuppressFunc: IgnoreMatchingColumnNameAndMaskingPolicyUsingFirstElem(),
Description: "Specifies the arguments to pass into the conditional masking policy SQL expression. The first column in the list specifies the column for the policy conditions to mask or tokenize the data and must match the column to which the masking policy is set. The additional columns specify the columns to evaluate to determine whether to mask or tokenize the data in each row of the query result when a query is made on the first column. If the USING clause is omitted, Snowflake treats the conditional masking policy as a normal masking policy.",
},
},
},
Expand Down Expand Up @@ -571,6 +572,7 @@ func extractPolicyWithColumnsList(v any, columnsKey string) (sdk.SchemaObjectIde
return sdk.SchemaObjectIdentifier{}, nil, err
}
if policyConfig[columnsKey] == nil {
// TODO: fix
return id, nil, fmt.Errorf("unable to extract policy with column list, unable to find columnsKey: %s", columnsKey)
}
columnsRaw := expandStringList(policyConfig[columnsKey].([]any))
Expand Down Expand Up @@ -779,7 +781,9 @@ func handleColumns(d ResourceValueSetter, columns []sdk.ViewDetails, policyRefs
projectionPolicy, err := collections.FindFirst(policyRefs, func(r sdk.PolicyReference) bool {
return r.PolicyKind == sdk.PolicyKindProjectionPolicy && r.RefColumnName != nil && *r.RefColumnName == column.Name
})
if err == nil {
if err != nil {
columnsRaw[i]["projection_policy"] = nil
} else {
if projectionPolicy.PolicyDb != nil && projectionPolicy.PolicySchema != nil {
columnsRaw[i]["projection_policy"] = []map[string]any{
{
Expand All @@ -793,6 +797,9 @@ func handleColumns(d ResourceValueSetter, columns []sdk.ViewDetails, policyRefs
maskingPolicy, err := collections.FindFirst(policyRefs, func(r sdk.PolicyReference) bool {
return r.PolicyKind == sdk.PolicyKindMaskingPolicy && r.RefColumnName != nil && *r.RefColumnName == column.Name
})
// if err != nil {
// columnsRaw[i]["masking_policy"] = nil
// } else {
if err == nil {
if maskingPolicy.PolicyDb != nil && maskingPolicy.PolicySchema != nil {
var usingArgs []string
Expand Down
112 changes: 112 additions & 0 deletions pkg/resources/view_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,118 @@ end;;
})
}

func TestAcc_View_columnsWithMaskingPolicyWithoutUsing(t *testing.T) {
t.Setenv(string(testenvs.ConfigureClientOnce), "")
_ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance)
acc.TestAccPreCheck(t)

id := acc.TestClient().Ids.RandomSchemaObjectIdentifier()
table, tableCleanup := acc.TestClient().Table.CreateWithColumns(t, []sdk.TableColumnRequest{
*sdk.NewTableColumnRequest("id", sdk.DataTypeNumber),
*sdk.NewTableColumnRequest("foo", sdk.DataTypeNumber),
*sdk.NewTableColumnRequest("bar", sdk.DataTypeNumber),
})
t.Cleanup(tableCleanup)
statement := fmt.Sprintf("SELECT id, foo FROM %s", table.ID().FullyQualifiedName())

maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyWithOptions(t,
[]sdk.TableColumnSignature{
{
Name: "One",
Type: sdk.DataTypeNumber,
},
},
sdk.DataTypeNumber,
`
case
when One > 0 then One
else 0
end;;
`,
new(sdk.CreateMaskingPolicyOptions),
)
t.Cleanup(maskingPolicyCleanup)

projectionPolicy, projectionPolicyCleanup := acc.TestClient().ProjectionPolicy.CreateProjectionPolicy(t)
t.Cleanup(projectionPolicyCleanup)

// generators currently don't handle lists of objects, so use the old way
basicView := func(columns ...string) config.Variables {
return config.Variables{
"name": config.StringVariable(id.Name()),
"database": config.StringVariable(id.DatabaseName()),
"schema": config.StringVariable(id.SchemaName()),
"statement": config.StringVariable(statement),
"column": config.SetVariable(
collections.Map(columns, func(columnName string) config.Variable {
return config.MapVariable(map[string]config.Variable{
"column_name": config.StringVariable(columnName),
})
})...,
),
}
}

basicViewWithPolicies := func() config.Variables {
conf := basicView("ID", "FOO")
delete(conf, "column")
conf["projection_name"] = config.StringVariable(projectionPolicy.FullyQualifiedName())
conf["masking_name"] = config.StringVariable(maskingPolicy.ID().FullyQualifiedName())
return conf
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: acc.CheckDestroy(t, resources.View),
Steps: []resource.TestStep{
// With all policies on columns
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/columns"),
ConfigVariables: basicViewWithPolicies(),
Check: assert.AssertThat(t,
resourceassert.ViewResource(t, "snowflake_view.test").
HasNameString(id.Name()).
HasStatementString(statement).
HasDatabaseString(id.DatabaseName()).
HasSchemaString(id.SchemaName()).
HasColumnLength(2),
objectassert.View(t, id).
HasMaskingPolicyReferences(acc.TestClient(), 1).
HasProjectionPolicyReferences(acc.TestClient(), 1),
),
},
// Remove policies on columns externally
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/columns"),
ConfigVariables: basicViewWithPolicies(),
PreConfig: func() {
acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetMaskingPolicyOnColumn(*sdk.NewViewUnsetColumnMaskingPolicyRequest("ID")))
acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetProjectionPolicyOnColumn(*sdk.NewViewUnsetProjectionPolicyRequest("ID")))
},
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate),
},
},
Check: assert.AssertThat(t,
resourceassert.ViewResource(t, "snowflake_view.test").
HasNameString(id.Name()).
HasStatementString(statement).
HasDatabaseString(id.DatabaseName()).
HasSchemaString(id.SchemaName()).
HasColumnLength(2),
objectassert.View(t, id).
HasMaskingPolicyReferences(acc.TestClient(), 1).
HasProjectionPolicyReferences(acc.TestClient(), 1),
),
},
},
})
}

func TestAcc_View_Rename(t *testing.T) {
t.Setenv(string(testenvs.ConfigureClientOnce), "")
statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES"
Expand Down

0 comments on commit 8395c60

Please sign in to comment.