diff --git a/examples/databricks/enhanced-security-compliance/README.md b/examples/databricks/enhanced-security-compliance/README.md new file mode 100644 index 000000000000..40415d305a73 --- /dev/null +++ b/examples/databricks/enhanced-security-compliance/README.md @@ -0,0 +1,7 @@ +## Example: Databricks Workspace with Enhanced Security and Compliance + +This example provisions a Databricks Workspace within Azure with Enhanced Security and Compliance settings enabled. + +### Variables + +* `prefix` - (Required) The prefix used for all resources in this example. diff --git a/examples/databricks/enhanced-security-compliance/main.tf b/examples/databricks/enhanced-security-compliance/main.tf new file mode 100644 index 000000000000..bfc62bb5c6c8 --- /dev/null +++ b/examples/databricks/enhanced-security-compliance/main.tf @@ -0,0 +1,23 @@ +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "${var.prefix}-databricks-esc" + location = "West Europe" +} + +resource "azurerm_databricks_workspace" "example" { + name = "${var.prefix}-DBW" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku = "premium" + managed_resource_group_name = "${var.prefix}-DBW-managed-esc" + + enhanced_security_compliance { + automatic_cluster_update_enabled = true + compliance_security_profile_enabled = true + compliance_security_profile_standards = ["HIPAA", "PCI_DSS"] + enhanced_security_monitoring_enabled = true + } +} diff --git a/examples/databricks/enhanced-security-compliance/variables.tf b/examples/databricks/enhanced-security-compliance/variables.tf new file mode 100644 index 000000000000..1da9fc1a8e74 --- /dev/null +++ b/examples/databricks/enhanced-security-compliance/variables.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "prefix" { + description = "The Prefix used for all resources in this example" +} + diff --git a/internal/services/databricks/databricks_workspace_data_source.go b/internal/services/databricks/databricks_workspace_data_source.go index 988dcccf7329..28d7111178c7 100644 --- a/internal/services/databricks/databricks_workspace_data_source.go +++ b/internal/services/databricks/databricks_workspace_data_source.go @@ -99,6 +99,34 @@ func dataSourceDatabricksWorkspace() *pluginsdk.Resource { }, }, + "enhanced_security_compliance": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "automatic_cluster_update_enabled": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + "compliance_security_profile_enabled": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + "compliance_security_profile_standards": { + Type: pluginsdk.TypeSet, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "enhanced_security_monitoring_enabled": { + Type: pluginsdk.TypeBool, + Computed: true, + }, + }, + }, + }, + "tags": commonschema.Tags(), }, } @@ -138,6 +166,9 @@ func dataSourceDatabricksWorkspaceRead(d *pluginsdk.ResourceData, meta interface } d.Set("workspace_url", model.Properties.WorkspaceURL) d.Set("location", model.Location) + if err := d.Set("enhanced_security_compliance", flattenWorkspaceEnhancedSecurity(model.Properties.EnhancedSecurityCompliance)); err != nil { + return fmt.Errorf("setting `enhanced_security_compliance`: %+v", err) + } return tags.FlattenAndSet(d, model.Tags) } diff --git a/internal/services/databricks/databricks_workspace_data_source_test.go b/internal/services/databricks/databricks_workspace_data_source_test.go index ba7ae5dcc7be..ce0ff08895e2 100644 --- a/internal/services/databricks/databricks_workspace_data_source_test.go +++ b/internal/services/databricks/databricks_workspace_data_source_test.go @@ -46,6 +46,27 @@ func TestAccDatabricksWorkspaceDataSource_storageAccountIdentity(t *testing.T) { }) } +func TestAccDatabricksWorkspaceDataSource_enhancedComplianceSecurity(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data), + Check: acceptance.ComposeTestCheckFunc( + acceptance.TestMatchResourceAttr(data.ResourceName, "workspace_url", regexp.MustCompile("azuredatabricks.net")), + check.That(data.ResourceName).Key("workspace_id").Exists(), + check.That(data.ResourceName).Key("location").Exists(), + check.That(data.ResourceName).Key("enhanced_security_compliance.#").HasValue("1"), + check.That(data.ResourceName).Key("enhanced_security_compliance.0.automatic_cluster_update_enabled").HasValue("true"), + check.That(data.ResourceName).Key("enhanced_security_compliance.0.compliance_security_profile_enabled").HasValue("true"), + check.That(data.ResourceName).Key("enhanced_security_compliance.0.compliance_security_profile_standards.#").HasValue("2"), + check.That(data.ResourceName).Key("enhanced_security_compliance.0.enhanced_security_monitoring_enabled").HasValue("true"), + ), + }, + }) +} + func (DatabricksWorkspaceDataSource) basic(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { @@ -195,3 +216,35 @@ resource "azurerm_key_vault_access_policy" "databricks" { } `, data.RandomInteger, data.Locations.Primary, data.RandomString, getDatabricksPrincipalId(data.Client().SubscriptionID)) } + +func (DatabricksWorkspaceDataSource) enhancedSecurityCompliance(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-databricks-%d" + location = "%s" +} + +resource "azurerm_databricks_workspace" "test" { + name = "acctestDBW-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "premium" + + enhanced_security_compliance { + automatic_cluster_update_enabled = true + compliance_security_profile_enabled = true + compliance_security_profile_standards = ["PCI_DSS", "HIPAA"] + enhanced_security_monitoring_enabled = true + } +} + +data "azurerm_databricks_workspace" "test" { + name = azurerm_databricks_workspace.test.name + resource_group_name = azurerm_resource_group.test.name +} + `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/internal/services/databricks/databricks_workspace_resource.go b/internal/services/databricks/databricks_workspace_resource.go index 8754002b4686..9bf6a6836b9e 100644 --- a/internal/services/databricks/databricks_workspace_resource.go +++ b/internal/services/databricks/databricks_workspace_resource.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" @@ -333,49 +334,124 @@ func resourceDatabricksWorkspace() *pluginsdk.Resource { }, }, + "enhanced_security_compliance": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "automatic_cluster_update_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "compliance_security_profile_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "compliance_security_profile_standards": { + Type: pluginsdk.TypeSet, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + string(workspaces.ComplianceStandardHIPAA), + string(workspaces.ComplianceStandardPCIDSS), + }, false), + }, + }, + "enhanced_security_monitoring_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + "tags": commonschema.Tags(), }, - CustomizeDiff: pluginsdk.CustomizeDiffShim(func(ctx context.Context, d *pluginsdk.ResourceDiff, v interface{}) error { - _, customerEncryptionEnabled := d.GetChange("customer_managed_key_enabled") - _, defaultStorageFirewallEnabled := d.GetChange("default_storage_firewall_enabled") - _, infrastructureEncryptionEnabled := d.GetChange("infrastructure_encryption_enabled") - _, publicNetworkAccess := d.GetChange("public_network_access_enabled") - _, requireNsgRules := d.GetChange("network_security_group_rules_required") - _, backendPool := d.GetChange("load_balancer_backend_address_pool_id") - _, managedServicesCMK := d.GetChange("managed_services_cmk_key_vault_key_id") - _, managedDiskCMK := d.GetChange("managed_disk_cmk_key_vault_key_id") - - oldSku, newSku := d.GetChange("sku") - - // Disabling Public Network Access means that this is a Private Endpoint Workspace - // Having a Load Balancer Backend Address Pool means the this is a Secure Cluster Connectivity Workspace - // You cannot have a Private Enpoint Workspace and a Secure Cluster Connectivity Workspace definitions in - // the same workspace configuration... - if !publicNetworkAccess.(bool) { - if requireNsgRules.(string) == string(workspaces.RequiredNsgRulesAllRules) { - return fmt.Errorf("having 'network_security_group_rules_required' set to %q and 'public_network_access_enabled' set to 'false' is an invalid configuration", string(workspaces.RequiredNsgRulesAllRules)) + CustomizeDiff: pluginsdk.CustomDiffWithAll( + pluginsdk.CustomizeDiffShim(func(ctx context.Context, d *pluginsdk.ResourceDiff, v interface{}) error { + _, customerEncryptionEnabled := d.GetChange("customer_managed_key_enabled") + _, defaultStorageFirewallEnabled := d.GetChange("default_storage_firewall_enabled") + _, infrastructureEncryptionEnabled := d.GetChange("infrastructure_encryption_enabled") + _, publicNetworkAccess := d.GetChange("public_network_access_enabled") + _, requireNsgRules := d.GetChange("network_security_group_rules_required") + _, backendPool := d.GetChange("load_balancer_backend_address_pool_id") + _, managedServicesCMK := d.GetChange("managed_services_cmk_key_vault_key_id") + _, managedDiskCMK := d.GetChange("managed_disk_cmk_key_vault_key_id") + _, enhancedSecurityCompliance := d.GetChange("enhanced_security_compliance") + + oldSku, newSku := d.GetChange("sku") + + // Disabling Public Network Access means that this is a Private Endpoint Workspace + // Having a Load Balancer Backend Address Pool means the this is a Secure Cluster Connectivity Workspace + // You cannot have a Private Enpoint Workspace and a Secure Cluster Connectivity Workspace definitions in + // the same workspace configuration... + if !publicNetworkAccess.(bool) { + if requireNsgRules.(string) == string(workspaces.RequiredNsgRulesAllRules) { + return fmt.Errorf("having `network_security_group_rules_required` set to %q and `public_network_access_enabled` set to `false` is an invalid configuration", string(workspaces.RequiredNsgRulesAllRules)) + } + if backendPool.(string) != "" { + return fmt.Errorf("having `load_balancer_backend_address_pool_id` defined and having `public_network_access_enabled` set to `false` is an invalid configuration") + } } - if backendPool.(string) != "" { - return fmt.Errorf("having 'load_balancer_backend_address_pool_id' defined and having 'public_network_access_enabled' set to 'false' is an invalid configuration") + + if d.HasChange("sku") { + if newSku == "trial" { + log.Printf("[DEBUG] recreate databricks workspace, cannot be migrated to %s", newSku) + d.ForceNew("sku") + } else { + log.Printf("[DEBUG] databricks workspace can be upgraded from %s to %s", oldSku, newSku) + } } - } - if d.HasChange("sku") { - if newSku == "trial" { - log.Printf("[DEBUG] recreate databricks workspace, cannot be migrated to %s", newSku) - d.ForceNew("sku") - } else { - log.Printf("[DEBUG] databricks workspace can be upgraded from %s to %s", oldSku, newSku) + if (customerEncryptionEnabled.(bool) || defaultStorageFirewallEnabled.(bool) || len(enhancedSecurityCompliance.([]interface{})) > 0 || infrastructureEncryptionEnabled.(bool) || managedServicesCMK.(string) != "" || managedDiskCMK.(string) != "") && !strings.EqualFold("premium", newSku.(string)) { + return fmt.Errorf("`customer_managed_key_enabled`, `default_storage_firewall_enabled`, `enhanced_security_compliance`, `infrastructure_encryption_enabled`, `managed_disk_cmk_key_vault_key_id` and `managed_services_cmk_key_vault_key_id` are only available with a `premium` workspace `sku`, got %q", newSku) } - } - if (customerEncryptionEnabled.(bool) || defaultStorageFirewallEnabled.(bool) || infrastructureEncryptionEnabled.(bool) || managedServicesCMK.(string) != "" || managedDiskCMK.(string) != "") && !strings.EqualFold("premium", newSku.(string)) { - return fmt.Errorf("'customer_managed_key_enabled', 'default_storage_firewall_enabled', 'infrastructure_encryption_enabled', 'managed_disk_cmk_key_vault_key_id' and 'managed_services_cmk_key_vault_key_id' are only available with a 'premium' workspace 'sku', got %q", newSku) - } + return nil + }), - return nil - }), + // Once compliance security profile has been enabled, disabling it will force a workspace replacement + pluginsdk.ForceNewIfChange("enhanced_security_compliance.0.compliance_security_profile_enabled", func(ctx context.Context, old, new, meta interface{}) bool { + return old.(bool) && !new.(bool) + }), + + // Once a compliance standard is enabled, disabling it will force a workspace replacement + pluginsdk.ForceNewIfChange("enhanced_security_compliance.0.compliance_security_profile_standards", func(ctx context.Context, old, new, meta interface{}) bool { + removedStandards := old.(*pluginsdk.Set).Difference(new.(*pluginsdk.Set)) + return removedStandards.Len() > 0 + }), + + // Compliance security profile requires automatic cluster update and enhanced security monitoring to be enabled + pluginsdk.CustomizeDiffShim(func(ctx context.Context, d *pluginsdk.ResourceDiff, v interface{}) error { + _, complianceSecurityProfileEnabled := d.GetChange("enhanced_security_compliance.0.compliance_security_profile_enabled") + _, automaticClusterUpdateEnabled := d.GetChange("enhanced_security_compliance.0.automatic_cluster_update_enabled") + _, enhancedSecurityMonitoringEnabled := d.GetChange("enhanced_security_compliance.0.enhanced_security_monitoring_enabled") + + if complianceSecurityProfileEnabled.(bool) && (!automaticClusterUpdateEnabled.(bool) || !enhancedSecurityMonitoringEnabled.(bool)) { + return fmt.Errorf("`automatic_cluster_update_enabled` and `enhanced_security_monitoring_enabled` must be set to true when `compliance_security_profile_enabled` is set to true") + } + + return nil + }), + + // compliance standards cannot be specified without enabling compliance profile + pluginsdk.CustomizeDiffShim(func(ctx context.Context, d *pluginsdk.ResourceDiff, v interface{}) error { + _, complianceSecurityProfileEnabled := d.GetChange("enhanced_security_compliance.0.compliance_security_profile_enabled") + _, complianceStandards := d.GetChange("enhanced_security_compliance.0.compliance_security_profile_standards") + + if !complianceSecurityProfileEnabled.(bool) && complianceStandards.(*pluginsdk.Set).Len() > 0 { + return fmt.Errorf("`compliance_security_profile_standards` cannot be set when `compliance_security_profile_enabled` is false") + } + + return nil + }), + ), } return resource @@ -469,16 +545,16 @@ func resourceDatabricksWorkspaceCreateUpdate(d *pluginsdk.ResourceData, meta int priSub := config["private_subnet_name"].(string) if config["virtual_network_id"].(string) == "" && (pubSub != "" || priSub != "") { - return fmt.Errorf("'public_subnet_name' and/or 'private_subnet_name' cannot be defined if 'virtual_network_id' is not set") + return fmt.Errorf("`public_subnet_name` and/or `private_subnet_name` cannot be defined if `virtual_network_id` is not set") } if config["virtual_network_id"].(string) != "" && (pubSub == "" || priSub == "") { - return fmt.Errorf("'public_subnet_name' and 'private_subnet_name' must both have values if 'virtual_network_id' is set") + return fmt.Errorf("`public_subnet_name` and `private_subnet_name` must both have values if `virtual_network_id` is set") } if pubSub != "" && pubSubAssoc == nil { - return fmt.Errorf("you must define a value for 'public_subnet_network_security_group_association_id' if 'public_subnet_name' is set") + return fmt.Errorf("you must define a value for `public_subnet_network_security_group_association_id` if `public_subnet_name` is set") } if priSub != "" && priSubAssoc == nil { - return fmt.Errorf("you must define a value for 'private_subnet_network_security_group_association_id' if 'private_subnet_name' is set") + return fmt.Errorf("you must define a value for `private_subnet_network_security_group_association_id` if `private_subnet_name` is set") } } @@ -645,6 +721,9 @@ func resourceDatabricksWorkspaceCreateUpdate(d *pluginsdk.ResourceData, meta int workspace.Properties.Encryption = encrypt } + enhancedSecurityCompliance := d.Get("enhanced_security_compliance") + workspace.Properties.EnhancedSecurityCompliance = expandWorkspaceEnhancedSecurity(enhancedSecurityCompliance.([]interface{})) + if err := client.CreateOrUpdateThenPoll(ctx, id, workspace); err != nil { return fmt.Errorf("creating/updating %s: %+v", id, err) } @@ -825,6 +904,8 @@ func resourceDatabricksWorkspaceRead(d *pluginsdk.ResourceData, meta interface{} } } + d.Set("enhanced_security_compliance", flattenWorkspaceEnhancedSecurity(model.Properties.EnhancedSecurityCompliance)) + var encryptDiskEncryptionSetId string if model.Properties.DiskEncryptionSetId != nil { encryptDiskEncryptionSetId = *model.Properties.DiskEncryptionSetId @@ -1084,3 +1165,82 @@ func workspaceCustomParametersString() []string { "custom_parameters.0.vnet_address_prefix", } } + +func flattenWorkspaceEnhancedSecurity(input *workspaces.EnhancedSecurityComplianceDefinition) []interface{} { + if input == nil { + return []interface{}{} + } + + enhancedSecurityCompliance := make(map[string]interface{}) + + if v := input.AutomaticClusterUpdate; v != nil { + enhancedSecurityCompliance["automatic_cluster_update_enabled"] = pointer.From(v.Value) != workspaces.AutomaticClusterUpdateValueDisabled + } + + if v := input.EnhancedSecurityMonitoring; v != nil { + enhancedSecurityCompliance["enhanced_security_monitoring_enabled"] = pointer.From(v.Value) != workspaces.EnhancedSecurityMonitoringValueDisabled + } + + if v := input.ComplianceSecurityProfile; v != nil { + enhancedSecurityCompliance["compliance_security_profile_enabled"] = pointer.From(v.Value) != workspaces.ComplianceSecurityProfileValueDisabled + + standards := pluginsdk.NewSet(pluginsdk.HashString, nil) + for _, s := range pointer.From(v.ComplianceStandards) { + if s == workspaces.ComplianceStandardNONE { + continue + } + standards.Add(string(s)) + } + + enhancedSecurityCompliance["compliance_security_profile_standards"] = standards + } + + return []interface{}{enhancedSecurityCompliance} +} + +func expandWorkspaceEnhancedSecurity(input []interface{}) *workspaces.EnhancedSecurityComplianceDefinition { + if len(input) == 0 || input[0] == nil { + return nil + } + + config := input[0].(map[string]interface{}) + + automaticClusterUpdateEnabled := workspaces.AutomaticClusterUpdateValueDisabled + if enabled, ok := config["automatic_cluster_update_enabled"].(bool); ok && enabled { + automaticClusterUpdateEnabled = workspaces.AutomaticClusterUpdateValueEnabled + } + + enhancedSecurityMonitoringEnabled := workspaces.EnhancedSecurityMonitoringValueDisabled + if enabled, ok := config["enhanced_security_monitoring_enabled"].(bool); ok && enabled { + enhancedSecurityMonitoringEnabled = workspaces.EnhancedSecurityMonitoringValueEnabled + } + + complianceSecurityProfileEnabled := workspaces.ComplianceSecurityProfileValueDisabled + if enabled, ok := config["compliance_security_profile_enabled"].(bool); ok && enabled { + complianceSecurityProfileEnabled = workspaces.ComplianceSecurityProfileValueEnabled + } + + complianceStandards := []workspaces.ComplianceStandard{} + if standardSet, ok := config["compliance_security_profile_standards"].(*pluginsdk.Set); ok { + for _, s := range standardSet.List() { + complianceStandards = append(complianceStandards, workspaces.ComplianceStandard(s.(string))) + } + } + + if complianceSecurityProfileEnabled == workspaces.ComplianceSecurityProfileValueEnabled && len(complianceStandards) == 0 { + complianceStandards = append(complianceStandards, workspaces.ComplianceStandardNONE) + } + + return &workspaces.EnhancedSecurityComplianceDefinition{ + AutomaticClusterUpdate: &workspaces.AutomaticClusterUpdateDefinition{ + Value: &automaticClusterUpdateEnabled, + }, + EnhancedSecurityMonitoring: &workspaces.EnhancedSecurityMonitoringDefinition{ + Value: &enhancedSecurityMonitoringEnabled, + }, + ComplianceSecurityProfile: &workspaces.ComplianceSecurityProfileDefinition{ + Value: &complianceSecurityProfileEnabled, + ComplianceStandards: &complianceStandards, + }, + } +} diff --git a/internal/services/databricks/databricks_workspace_resource_test.go b/internal/services/databricks/databricks_workspace_resource_test.go index afe1ef7f2965..b6e1bdf36eae 100644 --- a/internal/services/databricks/databricks_workspace_resource_test.go +++ b/internal/services/databricks/databricks_workspace_resource_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "regexp" "strings" "testing" @@ -400,6 +401,96 @@ func TestAccDatabricksWorkspace_altSubscriptionCmkDiskOnly(t *testing.T) { }) } +func TestAccDatabricksWorkspace_enhancedComplianceSecurity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", true, true, []string{"PCI_DSS", "HIPAA"}, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithoutStandards(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", true, true, []string{}, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithInvalidStandards(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", true, true, []string{"NONE"}, true), + ExpectError: regexp.MustCompile(`expected .* to be one of .*, got NONE`), + }, + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithInvalidStandardSku(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "standard", true, true, []string{"PCI_DSS", "HIPAA"}, true), + ExpectError: regexp.MustCompile("enhanced_security_compliance.*are only available with a `premium`"), + }, + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithoutEnhancedSecurityMonitoring(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", true, true, []string{"PCI_DSS", "HIPAA"}, false), + ExpectError: regexp.MustCompile("`enhanced_security_monitoring_enabled` must be set to true when `compliance_security_profile_enabled` is set to true"), + }, + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithoutAutomaticClusterUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", false, true, []string{"PCI_DSS", "HIPAA"}, true), + ExpectError: regexp.MustCompile("`automatic_cluster_update_enabled` .* must be set to true when `compliance_security_profile_enabled` is set to true"), + }, + }) +} + +func TestAccDatabricksWorkspace_enhancedComplianceSecurityWithInvalidComplianceSecurityProfile(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_databricks_workspace", "test") + r := DatabricksWorkspaceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enhancedSecurityCompliance(data, "premium", true, false, []string{"PCI_DSS", "HIPAA"}, true), + ExpectError: regexp.MustCompile("`compliance_security_profile_standards` cannot be set when `compliance_security_profile_enabled` is false"), + }, + }) +} + func getDatabricksPrincipalId(subscriptionId string) string { databricksPrincipalID := "bb9ef821-a78b-4312-90cc-5ece3fad3430" if strings.HasPrefix(strings.ToLower(subscriptionId), "85b3dbca") { @@ -2780,3 +2871,35 @@ resource "azurerm_key_vault_access_policy" "managed" { } `, data.RandomInteger, data.Locations.Secondary, data.RandomString, databricksPrincipalID, alt.tenant_id, alt.subscription_id) } + +func (DatabricksWorkspaceResource) enhancedSecurityCompliance(data acceptance.TestData, sku string, automaticClusterUpdateEnabled bool, complianceSecurityProfileEnabled bool, complianceSecurityProfileStandards []string, enhancedSecurityMonitoringEnabled bool) string { + complianceSecurityProfileStandardsStr := "" + if len(complianceSecurityProfileStandards) > 0 { + complianceSecurityProfileStandardsStr = fmt.Sprintf(`"%s"`, strings.Join(complianceSecurityProfileStandards, `", "`)) + } + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-databricks-%d" + location = "%s" +} + +resource "azurerm_databricks_workspace" "test" { + name = "acctestDBW-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "%s" + + enhanced_security_compliance { + automatic_cluster_update_enabled = %t + compliance_security_profile_enabled = %t + compliance_security_profile_standards = [%s] + enhanced_security_monitoring_enabled = %t + } +} + `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku, automaticClusterUpdateEnabled, complianceSecurityProfileEnabled, complianceSecurityProfileStandardsStr, enhancedSecurityMonitoringEnabled) +} diff --git a/website/docs/d/databricks_workspace.html.markdown b/website/docs/d/databricks_workspace.html.markdown index 0c01922ff9c5..bcea8ac9bb99 100644 --- a/website/docs/d/databricks_workspace.html.markdown +++ b/website/docs/d/databricks_workspace.html.markdown @@ -44,6 +44,8 @@ output "databricks_workspace_id" { * `storage_account_identity` - A `storage_account_identity` block as documented below. +* `enhanced_security_compliance` - An `enhanced_security_compliance` block as documented below. + * `tags` - A mapping of tags to assign to the Databricks Workspace. --- @@ -66,6 +68,18 @@ A `storage_account_identity` block exports the following: * `type` - The type of the internal databricks storage account. +--- + +An `enhanced_security_compliance` block exports the following: + +* `automatic_cluster_update_enabled` - Whether automatic cluster updates for this workspace is enabled. + +* `compliance_security_profile_enabled` - Whether compliance security profile for this workspace is enabled. + +* `compliance_security_profile_standards` - A list of standards enforced on this workspace. + +* `enhanced_security_monitoring_enabled` - Whether enhanced security monitoring for this workspace is enabled. + ## Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: diff --git a/website/docs/r/databricks_workspace.html.markdown b/website/docs/r/databricks_workspace.html.markdown index 3ffab227cb89..6710632c90f6 100644 --- a/website/docs/r/databricks_workspace.html.markdown +++ b/website/docs/r/databricks_workspace.html.markdown @@ -86,6 +86,8 @@ The following arguments are supported: * `custom_parameters` - (Optional) A `custom_parameters` block as documented below. +* `enhanced_security_compliance` - (Optional) An `enhanced_security_compliance` block as documented below. This feature is only valid if `sku` is set to `premium`. + * `tags` - (Optional) A mapping of tags to assign to the resource. --- @@ -122,6 +124,26 @@ A `custom_parameters` block supports the following: ~> **Note:** Databricks requires that a network security group is associated with the `public` and `private` subnets when a `virtual_network_id` has been defined. Both `public` and `private` subnets must be delegated to `Microsoft.Databricks/workspaces`. For more information about subnet delegation see the [product documentation](https://docs.microsoft.com/azure/virtual-network/subnet-delegation-overview). +--- + +An `enhanced_security_compliance` block supports the following: + +* `automatic_cluster_update_enabled` - (Optional) Enables automatic cluster updates for this workspace. Defaults to `false`. + +* `compliance_security_profile_enabled` - (Optional) Enables compliance security profile for this workspace. Defaults to `false`. + +~> **Note:** Changing the value of `compliance_security_profile_enabled` from `true` to `false` forces a replacement of the Databricks workspace. + +~> **Note:** The attributes `automatic_cluster_update_enabled` and `enhanced_security_monitoring_enabled` must be set to `true` in order to set `compliance_security_profile_enabled` to `true`. + +* `compliance_security_profile_standards` - (Optional) A list of standards to enforce on this workspace. Possible values include `HIPAA` and `PCI_DSS`. + +~> **Note:** `compliance_security_profile_enabled` must be set to `true` in order to use `compliance_security_profile_standards`. + +~> **Note:** Removing a standard from the `compliance_security_profile_standards` list forces a replacement of the Databricks workspace. + +* `enhanced_security_monitoring_enabled` - (Optional) Enables enhanced security monitoring for this workspace. Defaults to `false`. + ## Example HCL Configurations * [Databricks Workspace Secure Connectivity Cluster with Load Balancer](https://github.com/hashicorp/terraform-provider-azurerm/tree/main/examples/databricks/secure-connectivity-cluster/with-load-balancer)