Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_sql_database - Add sql threat detection policy configuration #1628

Merged
merged 9 commits into from
Sep 9, 2018
38 changes: 23 additions & 15 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
228 changes: 228 additions & 0 deletions azurerm/resource_arm_sql_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 would it make sense for this to default to Enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I left it Disabled is because that's the default value returned from the API if no policy is set.

Default values from the API:

{
    "properties": {
        "useServerDefault": "Disabled",
        "state": "Disabled",
        "disabledAlerts": "",
        "emailAddresses": "",
        "emailAccountAdmins": "Disabled",
        "storageEndpoint": "",
        "storageAccountAccessKey": "",
        "retentionDays": 0
    }
}

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
},
}
}

Expand All @@ -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{
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{})
Expand All @@ -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
}
Loading