diff --git a/azurerm/config.go b/azurerm/config.go index 08c4706dfd6d..f3d36f56992e 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -137,21 +137,22 @@ type ArmClient struct { iothubResourceClient devices.IotHubResourceClient // Databases - mysqlConfigurationsClient mysql.ConfigurationsClient - mysqlDatabasesClient mysql.DatabasesClient - mysqlFirewallRulesClient mysql.FirewallRulesClient - mysqlServersClient mysql.ServersClient - postgresqlConfigurationsClient postgresql.ConfigurationsClient - postgresqlDatabasesClient postgresql.DatabasesClient - postgresqlFirewallRulesClient postgresql.FirewallRulesClient - postgresqlServersClient postgresql.ServersClient - postgresqlVirtualNetworkRulesClient postgresql.VirtualNetworkRulesClient - sqlDatabasesClient sql.DatabasesClient - sqlElasticPoolsClient sql.ElasticPoolsClient - sqlFirewallRulesClient sql.FirewallRulesClient - sqlServersClient sql.ServersClient - sqlServerAzureADAdministratorsClient sql.ServerAzureADAdministratorsClient - sqlVirtualNetworkRulesClient sql.VirtualNetworkRulesClient + mysqlConfigurationsClient mysql.ConfigurationsClient + mysqlDatabasesClient mysql.DatabasesClient + mysqlFirewallRulesClient mysql.FirewallRulesClient + mysqlServersClient mysql.ServersClient + postgresqlConfigurationsClient postgresql.ConfigurationsClient + postgresqlDatabasesClient postgresql.DatabasesClient + postgresqlFirewallRulesClient postgresql.FirewallRulesClient + postgresqlServersClient postgresql.ServersClient + postgresqlVirtualNetworkRulesClient postgresql.VirtualNetworkRulesClient + sqlDatabasesClient sql.DatabasesClient + sqlDatabaseThreatDetectionPoliciesClient sql.DatabaseThreatDetectionPoliciesClient + sqlElasticPoolsClient sql.ElasticPoolsClient + sqlFirewallRulesClient sql.FirewallRulesClient + sqlServersClient sql.ServersClient + sqlServerAzureADAdministratorsClient sql.ServerAzureADAdministratorsClient + sqlVirtualNetworkRulesClient sql.VirtualNetworkRulesClient // Data Lake Store dataLakeStoreAccountClient storeAccount.AccountsClient @@ -627,6 +628,13 @@ func (c *ArmClient) registerDatabases(endpoint, subscriptionId string, auth auto c.configureClient(&sqlDBClient.Client, auth) c.sqlDatabasesClient = sqlDBClient + sqlDTDPClient := sql.NewDatabaseThreatDetectionPoliciesClientWithBaseURI(endpoint, subscriptionId) + setUserAgent(&sqlDTDPClient.Client) + sqlDTDPClient.Authorizer = auth + sqlDTDPClient.Sender = sender + sqlDTDPClient.SkipResourceProviderRegistration = c.skipProviderRegistration + c.sqlDatabaseThreatDetectionPoliciesClient = sqlDTDPClient + sqlFWClient := sql.NewFirewallRulesClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&sqlFWClient.Client, auth) c.sqlFirewallRulesClient = sqlFWClient diff --git a/azurerm/resource_arm_sql_database.go b/azurerm/resource_arm_sql_database.go index fc5b6f058620..e931b01616e9 100644 --- a/azurerm/resource_arm_sql_database.go +++ b/azurerm/resource_arm_sql_database.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" "github.com/satori/go.uuid" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) @@ -196,8 +197,113 @@ func resourceArmSqlDatabase() *schema.Resource { Computed: true, }, + "threat_detection_policy": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "disabled_alerts": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "Sql_Injection", + "Sql_Injection_Vulnerability", + "Access_Anomaly", + }, true), + }, + Set: schema.HashString, + }, + + "email_account_admins": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppress.CaseDifference, + Default: string(sql.SecurityAlertPolicyEmailAccountAdminsDisabled), + ValidateFunc: validation.StringInSlice([]string{ + string(sql.SecurityAlertPolicyEmailAccountAdminsDisabled), + string(sql.SecurityAlertPolicyEmailAccountAdminsEnabled), + }, true), + }, + + "email_addresses": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Set: schema.HashString, + }, + + "retention_days": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + }, + + "state": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppress.CaseDifference, + Default: string(sql.SecurityAlertPolicyStateDisabled), + ValidateFunc: validation.StringInSlice([]string{ + string(sql.SecurityAlertPolicyStateDisabled), + string(sql.SecurityAlertPolicyStateEnabled), + string(sql.SecurityAlertPolicyStateNew), + }, true), + }, + + "storage_account_access_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.NoZeroValues, + }, + + "storage_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.NoZeroValues, + }, + + "use_server_default": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppress.CaseDifference, + Default: string(sql.SecurityAlertPolicyUseServerDefaultDisabled), + ValidateFunc: validation.StringInSlice([]string{ + string(sql.SecurityAlertPolicyUseServerDefaultDisabled), + string(sql.SecurityAlertPolicyUseServerDefaultEnabled), + }, true), + }, + }, + }, + }, + "tags": tagsSchema(), }, + + CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error { + + threatDetection, hasThreatDetection := diff.GetOk("threat_detection_policy") + if hasThreatDetection { + if tl := threatDetection.([]interface{}); len(tl) > 0 { + t := tl[0].(map[string]interface{}) + + state := strings.ToLower(t["state"].(string)) + _, hasStorageEndpoint := t["storage_endpoint"] + _, hasStorageAccountAccessKey := t["storage_account_access_key"] + if state == "enabled" && !hasStorageEndpoint && !hasStorageAccountAccessKey { + return fmt.Errorf("`storage_endpoint` and `storage_account_access_key` are required when `state` is `Enabled`") + } + } + } + + return nil + }, } } @@ -212,6 +318,11 @@ func resourceArmSqlDatabaseCreateUpdate(d *schema.ResourceData, meta interface{} createMode := d.Get("create_mode").(string) tags := d.Get("tags").(map[string]interface{}) + threatDetection, err := expandArmSqlServerThreatDetectionPolicy(d, location) + if err != nil { + return fmt.Errorf("Error parsing the database threat detection policy: %+v", err) + } + properties := sql.Database{ Location: utils.String(location), DatabaseProperties: &sql.DatabaseProperties{ @@ -327,6 +438,11 @@ func resourceArmSqlDatabaseCreateUpdate(d *schema.ResourceData, meta interface{} d.SetId(*resp.ID) + threatDetectionClient := meta.(*ArmClient).sqlDatabaseThreatDetectionPoliciesClient + if _, err = threatDetectionClient.CreateOrUpdate(ctx, resourceGroup, serverName, name, *threatDetection); err != nil { + return fmt.Errorf("Error setting database threat detection policy: %+v", err) + } + return resourceArmSqlDatabaseRead(d, meta) } @@ -354,6 +470,15 @@ func resourceArmSqlDatabaseRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error making Read request on Sql Database %s: %+v", name, err) } + threatDetectionClient := meta.(*ArmClient).sqlDatabaseThreatDetectionPoliciesClient + threatDetection, err := threatDetectionClient.Get(ctx, resourceGroup, serverName, name) + if err == nil { + flattenedThreatDetection := flattenArmSqlServerThreatDetectionPolicy(d, threatDetection) + if err := d.Set("threat_detection_policy", flattenedThreatDetection); err != nil { + return fmt.Errorf("Error setting `threat_detection_policy`: %+v", err) + } + } + d.Set("name", resp.Name) d.Set("resource_group_name", resourceGroup) if location := resp.Location; location != nil { @@ -439,6 +564,56 @@ func flattenEncryptionStatus(encryption *[]sql.TransparentDataEncryption) string return "" } +func flattenArmSqlServerThreatDetectionPolicy(d *schema.ResourceData, policy sql.DatabaseSecurityAlertPolicy) []interface{} { + + // The SQL database threat detection API always returns the default value even if never set. + // If the values are on their default one, threat it as not set. + properties := policy.DatabaseSecurityAlertPolicyProperties + if properties == nil { + return []interface{}{} + } + + threatDetectionPolicy := make(map[string]interface{}) + + threatDetectionPolicy["state"] = string(properties.State) + threatDetectionPolicy["email_account_admins"] = string(properties.EmailAccountAdmins) + threatDetectionPolicy["use_server_default"] = string(properties.UseServerDefault) + + if disabledAlerts := properties.DisabledAlerts; disabledAlerts != nil { + flattenedAlerts := schema.NewSet(schema.HashString, []interface{}{}) + if v := *disabledAlerts; v != "" { + parsedAlerts := strings.Split(v, ";") + for _, a := range parsedAlerts { + flattenedAlerts.Add(a) + } + } + threatDetectionPolicy["disabled_alerts"] = flattenedAlerts + } + if emailAddresses := properties.EmailAddresses; emailAddresses != nil { + flattenedEmails := schema.NewSet(schema.HashString, []interface{}{}) + if v := *emailAddresses; v != "" { + parsedEmails := strings.Split(*emailAddresses, ";") + for _, e := range parsedEmails { + flattenedEmails.Add(e) + } + } + threatDetectionPolicy["email_addresses"] = flattenedEmails + } + if properties.StorageEndpoint != nil { + threatDetectionPolicy["storage_endpoint"] = *properties.StorageEndpoint + } + if properties.RetentionDays != nil { + threatDetectionPolicy["retention_days"] = int(*properties.RetentionDays) + } + + // If storage account access key is in state read it to the new state, as the API does not return it for security reasons + if v, ok := d.GetOk("threat_detection_policy.0.storage_account_access_key"); ok { + threatDetectionPolicy["storage_account_access_key"] = v.(string) + } + + return []interface{}{threatDetectionPolicy} +} + func expandAzureRmSqlDatabaseImport(d *schema.ResourceData) sql.ImportExtensionRequest { v := d.Get("import") dbimportRefs := v.([]interface{}) @@ -456,3 +631,56 @@ func expandAzureRmSqlDatabaseImport(d *schema.ResourceData) sql.ImportExtensionR }, } } + +func expandArmSqlServerThreatDetectionPolicy(d *schema.ResourceData, location string) (*sql.DatabaseSecurityAlertPolicy, error) { + policy := sql.DatabaseSecurityAlertPolicy{ + Location: utils.String(location), + DatabaseSecurityAlertPolicyProperties: &sql.DatabaseSecurityAlertPolicyProperties{ + State: sql.SecurityAlertPolicyStateDisabled, + }, + } + properties := policy.DatabaseSecurityAlertPolicyProperties + + td, ok := d.GetOk("threat_detection_policy") + if !ok { + return &policy, nil + } + + if tdl := td.([]interface{}); len(tdl) > 0 { + threatDetection := tdl[0].(map[string]interface{}) + + properties.State = sql.SecurityAlertPolicyState(threatDetection["state"].(string)) + properties.EmailAccountAdmins = sql.SecurityAlertPolicyEmailAccountAdmins(threatDetection["email_account_admins"].(string)) + properties.UseServerDefault = sql.SecurityAlertPolicyUseServerDefault(threatDetection["use_server_default"].(string)) + + if v, ok := threatDetection["disabled_alerts"]; ok { + alerts := v.(*schema.Set).List() + expandedAlerts := make([]string, len(alerts)) + for i, a := range alerts { + expandedAlerts[i] = a.(string) + } + properties.DisabledAlerts = utils.String(strings.Join(expandedAlerts, ";")) + } + if v, ok := threatDetection["email_addresses"]; ok { + emails := v.(*schema.Set).List() + expandedEmails := make([]string, len(emails)) + for i, e := range emails { + expandedEmails[i] = e.(string) + } + properties.EmailAddresses = utils.String(strings.Join(expandedEmails, ";")) + } + if v, ok := threatDetection["retention_days"]; ok { + properties.RetentionDays = utils.Int32(int32(v.(int))) + } + if v, ok := threatDetection["storage_account_access_key"]; ok { + properties.StorageAccountAccessKey = utils.String(v.(string)) + } + if v, ok := threatDetection["storage_endpoint"]; ok { + properties.StorageEndpoint = utils.String(v.(string)) + } + + return &policy, nil + } + + return &policy, nil +} diff --git a/azurerm/resource_arm_sql_database_test.go b/azurerm/resource_arm_sql_database_test.go index 43644d3f6a06..9f6fab40f3c5 100644 --- a/azurerm/resource_arm_sql_database_test.go +++ b/azurerm/resource_arm_sql_database_test.go @@ -215,6 +215,47 @@ func TestAccAzureRMSqlDatabase_requestedServiceObjectiveName(t *testing.T) { }) } +func TestAccAzureRMSqlDatabase_threatDetectionPolicy(t *testing.T) { + resourceName := "azurerm_sql_database.test" + ri := acctest.RandInt() + location := testLocation() + preConfig := testAccAzureRMSqlDatabase_threatDetectionPolicy(ri, location, "Enabled") + postConfig := testAccAzureRMSqlDatabase_threatDetectionPolicy(ri, location, "Disabled") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSqlDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.0.state", "Enabled"), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.0.retention_days", "15"), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.0.disabled_alerts.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.0.email_account_admins", "Enabled"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"create_mode", "threat_detection_policy.0.storage_account_access_key"}, + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSqlDatabaseExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "threat_detection_policy.0.state", "Disabled"), + ), + }, + }, + }) +} + func testCheckAzureRMSqlDatabaseExists(name string) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -644,3 +685,49 @@ resource "azurerm_sql_database" "test" { } `, rInt, location, rInt, rInt, requestedServiceObjectiveName) } + +func testAccAzureRMSqlDatabase_threatDetectionPolicy(rInt int, location, state string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "test%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + account_tier = "Standard" + account_replication_type = "GRS" + } + +resource "azurerm_sql_server" "test" { + name = "acctestsqlserver%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" + version = "12.0" + administrator_login = "mradministrator" + administrator_login_password = "thisIsDog11" +} + +resource "azurerm_sql_database" "test" { + name = "acctestdb%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_sql_server.test.name}" + location = "${azurerm_resource_group.test.location}" + edition = "Standard" + collation = "SQL_Latin1_General_CP1_CI_AS" + max_size_bytes = "1073741824" + + threat_detection_policy { + retention_days = 15 + state = "%s" + disabled_alerts = ["Sql_Injection"] + email_account_admins = "Enabled" + storage_account_access_key = "${azurerm_storage_account.test.primary_access_key}" + storage_endpoint = "${azurerm_storage_account.test.primary_blob_endpoint}" + use_server_default = "Disabled" + } +} +`, rInt, location, rInt, rInt, rInt, state) +} diff --git a/website/docs/r/sql_database.html.markdown b/website/docs/r/sql_database.html.markdown index 3acdce6b24e5..eebe525316a5 100644 --- a/website/docs/r/sql_database.html.markdown +++ b/website/docs/r/sql_database.html.markdown @@ -73,6 +73,8 @@ The following arguments are supported: * `elastic_pool_name` - (Optional) The name of the elastic database pool. +* `threat_detection_policy` - (Optional) Threat detection policy configuration. The `threat_detection_policy` block supports fields documented below. + * `tags` - (Optional) A mapping of tags to assign to the resource. `import` supports the following: @@ -85,6 +87,19 @@ The following arguments are supported: * `authentication_type` - (Required) Specifies the type of authentication used to access the server. Valid values are `SQL` or `ADPassword`. * `operation_mode` - (Optional) Specifies the type of import operation being performed. The only allowable value is `Import`. +--- + +`threat_detection_policy` supports the following: + +* `state` - (Required) The State of the Policy. Possible values are `Enabled`, `Disabled` or `New`. +* `disabled_alerts` - (Optional) Specifies a list of alerts which should be disabled. Possible values include `Access_Anomaly`, `Sql_Injection` and `Sql_Injection_Vulnerability`. +* `email_account_admins` - (Optional) Should the account administrators be emailed when this alert is triggered? +* `email_addresses` - (Optional) A list of email addresses which alerts should be sent to. +* `retention_days` - (Optional) Specifies the number of days to keep in the Threat Detection audit logs. +* `storage_account_access_key` - (Optional) Specifies the identifier key of the Threat Detection audit storage account. Required if `state` is `Enabled`. +* `storage_endpoint` - (Optional) Specifies the blob storage endpoint (e.g. https://MyAccount.blob.core.windows.net). This blob storage will hold all Threat Detection audit logs. Required if `state` is `Enabled`. +* `use_server_default` - (Optional) Should the default server policy be used? Defaults to `Disabled`. + ## Attributes Reference The following attributes are exported: