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_mysql_server - support for threat_detection_policy #7156

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions azurerm/internal/services/mysql/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
)

type Client struct {
ConfigurationsClient *mysql.ConfigurationsClient
DatabasesClient *mysql.DatabasesClient
FirewallRulesClient *mysql.FirewallRulesClient
ServersClient *mysql.ServersClient
VirtualNetworkRulesClient *mysql.VirtualNetworkRulesClient
ConfigurationsClient *mysql.ConfigurationsClient
DatabasesClient *mysql.DatabasesClient
FirewallRulesClient *mysql.FirewallRulesClient
ServersClient *mysql.ServersClient
ServerSecurityAlertPoliciesClient *mysql.ServerSecurityAlertPoliciesClient
VirtualNetworkRulesClient *mysql.VirtualNetworkRulesClient
}

func NewClient(o *common.ClientOptions) *Client {
Expand All @@ -26,14 +27,18 @@ func NewClient(o *common.ClientOptions) *Client {
ServersClient := mysql.NewServersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&ServersClient.Client, o.ResourceManagerAuthorizer)

serverSecurityAlertPoliciesClient := mysql.NewServerSecurityAlertPoliciesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&serverSecurityAlertPoliciesClient.Client, o.ResourceManagerAuthorizer)

VirtualNetworkRulesClient := mysql.NewVirtualNetworkRulesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&VirtualNetworkRulesClient.Client, o.ResourceManagerAuthorizer)

return &Client{
ConfigurationsClient: &ConfigurationsClient,
DatabasesClient: &DatabasesClient,
FirewallRulesClient: &FirewallRulesClient,
ServersClient: &ServersClient,
VirtualNetworkRulesClient: &VirtualNetworkRulesClient,
ConfigurationsClient: &ConfigurationsClient,
DatabasesClient: &DatabasesClient,
FirewallRulesClient: &FirewallRulesClient,
ServersClient: &ServersClient,
ServerSecurityAlertPoliciesClient: &serverSecurityAlertPoliciesClient,
VirtualNetworkRulesClient: &VirtualNetworkRulesClient,
}
}
196 changes: 196 additions & 0 deletions azurerm/internal/services/mysql/mysql_server_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,70 @@ func resourceArmMySqlServer() *schema.Resource {
},
},

"threat_detection_policy": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Optional: true,
},

"disabled_alerts": {
Type: schema.TypeSet,
Optional: true,
Set: schema.HashString,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{
"Sql_Injection",
"Sql_Injection_Vulnerability",
"Access_Anomaly",
"Data_Exfiltration",
"Unsafe_Action",
}, false),
},
},

"email_account_admins": {
Type: schema.TypeBool,
Optional: true,
},

"email_addresses": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
// todo email validation in code
},
Set: schema.HashString,
},

"retention_days": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(0),
},

"storage_account_access_key": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ValidateFunc: validation.StringIsNotEmpty,
},

"storage_endpoint": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringIsNotEmpty,
},
},
},
},

"tags": tags.Schema(),

"version": {
Expand Down Expand Up @@ -301,6 +365,7 @@ func resourceArmMySqlServer() *schema.Resource {

func resourceArmMySqlServerCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).MySQL.ServersClient
securityClient := meta.(*clients.Client).MySQL.ServerSecurityAlertPoliciesClient
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand Down Expand Up @@ -452,11 +517,26 @@ func resourceArmMySqlServerCreate(d *schema.ResourceData, meta interface{}) erro

d.SetId(*read.ID)

if v, ok := d.GetOk("threat_detection_policy"); ok {
alert := expandSecurityAlertPolicy(v)
if alert != nil {
future, err := securityClient.CreateOrUpdate(ctx, resourceGroup, name, *alert)
if err != nil {
return fmt.Errorf("error updataing mysql server security alert policy: %v", err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("error waiting for creation/update of mysql server security alert policy (server %q, resource group %q): %+v", name, resourceGroup, err)
}
}
}

return resourceArmMySqlServerRead(d, meta)
}

func resourceArmMySqlServerUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).MySQL.ServersClient
securityClient := meta.(*clients.Client).MySQL.ServerSecurityAlertPoliciesClient
ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand Down Expand Up @@ -519,11 +599,26 @@ func resourceArmMySqlServerUpdate(d *schema.ResourceData, meta interface{}) erro

d.SetId(*read.ID)

if v, ok := d.GetOk("threat_detection_policy"); ok {
alert := expandSecurityAlertPolicy(v)
if alert != nil {
future, err := securityClient.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, *alert)
if err != nil {
return fmt.Errorf("error updataing mysql server security alert policy: %v", err)
}

if err = future.WaitForCompletionRef(ctx, client.Client); err != nil {
return fmt.Errorf("error waiting for creation/update of mysql server security alert policy (server %q, resource group %q): %+v", id.Name, id.ResourceGroup, err)
}
}
}

return resourceArmMySqlServerRead(d, meta)
}

func resourceArmMySqlServerRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).MySQL.ServersClient
securityClient := meta.(*clients.Client).MySQL.ServerSecurityAlertPoliciesClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

Expand All @@ -550,8 +645,10 @@ func resourceArmMySqlServerRead(d *schema.ResourceData, meta interface{}) error
d.Set("location", azure.NormalizeLocation(*location))
}

tier := mysql.Basic
if sku := resp.Sku; sku != nil {
d.Set("sku_name", sku.Name)
tier = sku.Tier
}

if props := resp.ServerProperties; props != nil {
Expand All @@ -578,6 +675,21 @@ func resourceArmMySqlServerRead(d *schema.ResourceData, meta interface{}) error
d.Set("fqdn", props.FullyQualifiedDomainName)
}

// the basic does not support threat detection policies
if tier == mysql.GeneralPurpose || tier == mysql.MemoryOptimized {
secResp, err := securityClient.Get(ctx, id.ResourceGroup, id.Name)
if err != nil && !utils.ResponseWasNotFound(secResp.Response) {
return fmt.Errorf("error making read request to mysql server security alert policy: %+v", err)
}

if !utils.ResponseWasNotFound(secResp.Response) {
block := flattenSecurityAlertPolicy(secResp.SecurityAlertPolicyProperties, d.Get("threat_detection_policy.0.storage_account_access_key").(string))
if err := d.Set("threat_detection_policy", block); err != nil {
return fmt.Errorf("setting `threat_detection_policy`: %+v", err)
}
}
}

return tags.FlattenAndSet(d, resp.Tags)
}

Expand Down Expand Up @@ -696,3 +808,87 @@ func flattenMySQLStorageProfile(resp *mysql.StorageProfile) []interface{} {

return []interface{}{values}
}

func expandSecurityAlertPolicy(i interface{}) *mysql.ServerSecurityAlertPolicy {
slice := i.([]interface{})
if len(slice) == 0 {
return nil
}

block := slice[0].(map[string]interface{})

state := mysql.ServerSecurityAlertPolicyStateEnabled
if !block["enabled"].(bool) {
state = mysql.ServerSecurityAlertPolicyStateDisabled
}

props := &mysql.SecurityAlertPolicyProperties{
State: state,
}

if v, ok := block["disabled_alerts"]; ok {
props.DisabledAlerts = utils.ExpandStringSlice(v.(*schema.Set).List())
}

if v, ok := block["email_addresses"]; ok {
props.EmailAddresses = utils.ExpandStringSlice(v.(*schema.Set).List())
}

if v, ok := block["email_account_admins"]; ok {
props.EmailAccountAdmins = utils.Bool(v.(bool))
}

if v, ok := block["retention_days"]; ok {
props.RetentionDays = utils.Int32(int32(v.(int)))
}

if v, ok := block["storage_account_access_key"]; ok && v.(string) != "" {
props.StorageAccountAccessKey = utils.String(v.(string))
}

if v, ok := block["storage_endpoint"]; ok && v.(string) != "" {
props.StorageEndpoint = utils.String(v.(string))
}

return &mysql.ServerSecurityAlertPolicy{
SecurityAlertPolicyProperties: props,
}
}

func flattenSecurityAlertPolicy(props *mysql.SecurityAlertPolicyProperties, accessKey string) interface{} {
if props == nil {
return nil
}

// check if its an empty block as in its never been set before
if props.DisabledAlerts != nil && len(*props.DisabledAlerts) == 1 && (*props.DisabledAlerts)[0] == "" &&
props.EmailAddresses != nil && len(*props.EmailAddresses) == 1 && (*props.EmailAddresses)[0] == "" &&
props.StorageAccountAccessKey != nil && *props.StorageAccountAccessKey == "" &&
props.StorageEndpoint != nil && *props.StorageEndpoint == "" &&
props.RetentionDays != nil && *props.RetentionDays == 0 &&
props.EmailAccountAdmins != nil && !*props.EmailAccountAdmins &&
props.State == mysql.ServerSecurityAlertPolicyStateDisabled {
return nil
}

block := map[string]interface{}{}

block["enabled"] = props.State == mysql.ServerSecurityAlertPolicyStateEnabled

block["disabled_alerts"] = utils.FlattenStringSlice(props.DisabledAlerts)
block["email_addresses"] = utils.FlattenStringSlice(props.EmailAddresses)

if v := props.EmailAccountAdmins; v != nil {
block["email_account_admins"] = *v
}
if v := props.RetentionDays; v != nil {
block["retention_days"] = *v
}
if v := props.StorageEndpoint; v != nil {
block["storage_endpoint"] = *v
}

block["storage_account_access_key"] = accessKey

return []interface{}{block}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ func TestAccAzureRMMySQLServer_update(t *testing.T) {
),
},
data.ImportStep("administrator_login_password"), // not returned as sensitive
{
Config: testAccAzureRMMySQLServer_complete2(data, mysqlVersion),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMMySQLServerExists(data.ResourceName),
),
},
data.ImportStep("administrator_login_password", "threat_detection_policy.0.storage_account_access_key"),
{
Config: testAccAzureRMMySQLServer_basic(data, mysqlVersion),
Check: resource.ComposeTestCheckFunc(
Expand Down Expand Up @@ -449,12 +456,12 @@ provider "azurerm" {
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-%d"
location = "%s"
name = "acctestRG-%[1]d"
location = "%[2]s"
}

resource "azurerm_mysql_server" "test" {
name = "acctestmysqlsvr-%d"
name = "acctestmysqlsvr-%[1]d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
sku_name = "GP_Gen5_2"
Expand All @@ -466,9 +473,62 @@ resource "azurerm_mysql_server" "test" {
geo_redundant_backup_enabled = false
ssl_enforcement_enabled = true
storage_mb = 51200
version = "%s"
version = "%[3]s"
threat_detection_policy {
enabled = true
disabled_alerts = ["Sql_Injection", "Data_Exfiltration"]
email_account_admins = true
email_addresses = ["[email protected]", "[email protected]"]
retention_days = 7
}
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version)
`, data.RandomInteger, data.Locations.Primary, version)
}

func testAccAzureRMMySQLServer_complete2(data acceptance.TestData, version string) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-mysql-%[1]d"
location = "%[2]s"
}

resource "azurerm_storage_account" "test" {
name = "accsa%[1]d"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
account_tier = "Standard"
account_replication_type = "GRS"
}

resource "azurerm_mysql_server" "test" {
name = "acctestmysqlsvr-%[1]d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
sku_name = "GP_Gen5_2"
administrator_login = "acctestun"
administrator_login_password = "H@Sh1CoR3!updated"
auto_grow_enabled = true
backup_retention_days = 7
create_mode = "Default"
geo_redundant_backup_enabled = false
ssl_enforcement_enabled = true
storage_mb = 51200
version = "%[3]s"
threat_detection_policy {
enabled = true
disabled_alerts = ["Sql_Injection"]
email_account_admins = true
email_addresses = ["[email protected]"]
retention_days = 7
storage_endpoint = azurerm_storage_account.test.primary_blob_endpoint
storage_account_access_key = azurerm_storage_account.test.primary_access_key
}
}
`, data.RandomInteger, data.Locations.Primary, version)
}

func testAccAzureRMMySQLServer_completeDeprecated(data acceptance.TestData, version string) string { // remove in v3.0
Expand Down
Loading