diff --git a/azurerm/internal/services/mssql/mssql_server_security_alert_policy_resource.go b/azurerm/internal/services/mssql/mssql_server_security_alert_policy_resource.go index bb8ce87b0277..4f2e9fc649fe 100644 --- a/azurerm/internal/services/mssql/mssql_server_security_alert_policy_resource.go +++ b/azurerm/internal/services/mssql/mssql_server_security_alert_policy_resource.go @@ -14,6 +14,8 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) +// todo 3.0 - this may want to be put into the mssql_server resource now that it exists. + func resourceArmMssqlServerSecurityAlertPolicy() *schema.Resource { return &schema.Resource{ Create: resourceArmMssqlServerSecurityAlertPolicyCreateUpdate, diff --git a/azurerm/internal/services/mssql/tests/mssql_server_security_alert_policy_resource_test.go b/azurerm/internal/services/mssql/tests/mssql_server_security_alert_policy_resource_test.go index 53a8304b7144..a2c063ec9e3d 100644 --- a/azurerm/internal/services/mssql/tests/mssql_server_security_alert_policy_resource_test.go +++ b/azurerm/internal/services/mssql/tests/mssql_server_security_alert_policy_resource_test.go @@ -184,9 +184,9 @@ resource "azurerm_sql_server" "test" { resource "azurerm_storage_account" "test" { name = "accsa%d" resource_group_name = azurerm_resource_group.test.name - location = "%s" + location = azurerm_resource_group.test.location account_tier = "Standard" account_replication_type = "GRS" } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.Locations.Primary) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) } diff --git a/azurerm/internal/services/postgres/client/client.go b/azurerm/internal/services/postgres/client/client.go index 50975fc5276c..0dd71d9cc304 100644 --- a/azurerm/internal/services/postgres/client/client.go +++ b/azurerm/internal/services/postgres/client/client.go @@ -6,11 +6,12 @@ import ( ) type Client struct { - ConfigurationsClient *postgresql.ConfigurationsClient - DatabasesClient *postgresql.DatabasesClient - FirewallRulesClient *postgresql.FirewallRulesClient - ServersClient *postgresql.ServersClient - VirtualNetworkRulesClient *postgresql.VirtualNetworkRulesClient + ConfigurationsClient *postgresql.ConfigurationsClient + DatabasesClient *postgresql.DatabasesClient + FirewallRulesClient *postgresql.FirewallRulesClient + ServersClient *postgresql.ServersClient + ServerSecurityAlertPoliciesClient *postgresql.ServerSecurityAlertPoliciesClient + VirtualNetworkRulesClient *postgresql.VirtualNetworkRulesClient } func NewClient(o *common.ClientOptions) *Client { @@ -26,14 +27,18 @@ func NewClient(o *common.ClientOptions) *Client { serversClient := postgresql.NewServersClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&serversClient.Client, o.ResourceManagerAuthorizer) + serverSecurityAlertPoliciesClient := postgresql.NewServerSecurityAlertPoliciesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&serverSecurityAlertPoliciesClient.Client, o.ResourceManagerAuthorizer) + virtualNetworkRulesClient := postgresql.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, } } diff --git a/azurerm/internal/services/postgres/postgresql_server_resource.go b/azurerm/internal/services/postgres/postgresql_server_resource.go index 7f8cc2c45208..68831a6d08e1 100644 --- a/azurerm/internal/services/postgres/postgresql_server_resource.go +++ b/azurerm/internal/services/postgres/postgresql_server_resource.go @@ -277,6 +277,70 @@ func resourceArmPostgreSQLServer() *schema.Resource { DiffSuppressFunc: suppress.CaseDifference, }, + "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, + }, + }, + }, + }, + "fqdn": { Type: schema.TypeString, Computed: true, @@ -289,6 +353,7 @@ func resourceArmPostgreSQLServer() *schema.Resource { func resourceArmPostgreSQLServerCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Postgres.ServersClient + securityClient := meta.(*clients.Client).Postgres.ServerSecurityAlertPoliciesClient ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -339,7 +404,7 @@ func resourceArmPostgreSQLServerCreate(d *schema.ResourceData, meta interface{}) ssl = postgresql.SslEnforcementEnumDisabled } - storage := expandAzureRmPostgreSQLStorageProfile(d) + storage := expandPostgreSQLStorageProfile(d) var props postgresql.BasicServerPropertiesForCreate switch mode { @@ -440,11 +505,26 @@ func resourceArmPostgreSQLServerCreate(d *schema.ResourceData, meta interface{}) 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 postgres server security alert policy: %v", err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("error waiting for creation/update of postgrest server security alert policy (server %q, resource group %q): %+v", name, resourceGroup, err) + } + } + } + return resourceArmPostgreSQLServerRead(d, meta) } func resourceArmPostgreSQLServerUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Postgres.ServersClient + securityClient := meta.(*clients.Client).Postgres.ServerSecurityAlertPoliciesClient ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -478,7 +558,7 @@ func resourceArmPostgreSQLServerUpdate(d *schema.ResourceData, meta interface{}) AdministratorLoginPassword: utils.String(d.Get("administrator_login_password").(string)), PublicNetworkAccess: publicAccess, SslEnforcement: ssl, - StorageProfile: expandAzureRmPostgreSQLStorageProfile(d), + StorageProfile: expandPostgreSQLStorageProfile(d), Version: postgresql.ServerVersion(d.Get("version").(string)), }, Sku: sku, @@ -494,21 +574,26 @@ func resourceArmPostgreSQLServerUpdate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("waiting for update of PostgreSQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } - read, err := client.Get(ctx, id.ResourceGroup, id.Name) - if err != nil { - return fmt.Errorf("retrieving PostgreSQL Server %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) - } - if read.ID == nil { - return fmt.Errorf("Cannot read PostgreSQL Server %s (resource group %s) ID", id.Name, id.ResourceGroup) - } + 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 mssql server security alert policy: %v", err) + } - d.SetId(*read.ID) + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("error waiting for creation/update of postgrest server security alert policy (server %q, resource group %q): %+v", id.Name, id.ResourceGroup, err) + } + } + } return resourceArmPostgreSQLServerRead(d, meta) } func resourceArmPostgreSQLServerRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Postgres.ServersClient + securityClient := meta.(*clients.Client).Postgres.ServerSecurityAlertPoliciesClient ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() @@ -563,6 +648,19 @@ func resourceArmPostgreSQLServerRead(d *schema.ResourceData, meta interface{}) e // Computed d.Set("fqdn", props.FullyQualifiedDomainName) } + + secResp, err := securityClient.Get(ctx, id.ResourceGroup, id.Name) + if err != nil && !utils.ResponseWasNotFound(secResp.Response) { + return fmt.Errorf("error making read request to postgres 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) } @@ -627,7 +725,7 @@ func expandServerSkuName(skuName string) (*postgresql.Sku, error) { }, nil } -func expandAzureRmPostgreSQLStorageProfile(d *schema.ResourceData) *postgresql.StorageProfile { +func expandPostgreSQLStorageProfile(d *schema.ResourceData) *postgresql.StorageProfile { storage := postgresql.StorageProfile{} if v, ok := d.GetOk("storage_profile"); ok { storageprofile := v.([]interface{})[0].(map[string]interface{}) @@ -682,3 +780,87 @@ func flattenPostgreSQLStorageProfile(resp *postgresql.StorageProfile) []interfac return []interface{}{values} } + +func expandSecurityAlertPolicy(i interface{}) *postgresql.ServerSecurityAlertPolicy { + slice := i.([]interface{}) + if len(slice) == 0 { + return nil + } + + block := slice[0].(map[string]interface{}) + + state := postgresql.ServerSecurityAlertPolicyStateEnabled + if !block["enabled"].(bool) { + state = postgresql.ServerSecurityAlertPolicyStateDisabled + } + + props := &postgresql.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 &postgresql.ServerSecurityAlertPolicy{ + SecurityAlertPolicyProperties: props, + } +} + +func flattenSecurityAlertPolicy(props *postgresql.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 == postgresql.ServerSecurityAlertPolicyStateDisabled { + return nil + } + + block := map[string]interface{}{} + + block["enabled"] = props.State == postgresql.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} +} diff --git a/azurerm/internal/services/postgres/tests/postgresql_server_resource_test.go b/azurerm/internal/services/postgres/tests/postgresql_server_resource_test.go index f0d93a0a0d0d..5e5954245470 100644 --- a/azurerm/internal/services/postgres/tests/postgresql_server_resource_test.go +++ b/azurerm/internal/services/postgres/tests/postgresql_server_resource_test.go @@ -225,6 +225,13 @@ func TestAccAzureRMPostgreSQLServer_updated(t *testing.T) { ), }, data.ImportStep("administrator_login_password"), + { + Config: testAccAzureRMPostgreSQLServer_complete2(data, "9.6"), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLServerExists(data.ResourceName), + ), + }, + data.ImportStep("administrator_login_password"), { Config: testAccAzureRMPostgreSQLServer_basic(data, "9.6"), Check: resource.ComposeTestCheckFunc( @@ -501,7 +508,7 @@ resource "azurerm_postgresql_server" "import" { sku_name = azurerm_postgresql_server.test.sku_name version = azurerm_postgresql_server.test.version - storage_mb = azurerm_postgresql_server.test.storage_profile.storage_mb + storage_mb = azurerm_postgresql_server.test.storage_mb ssl_enforcement_enabled = azurerm_postgresql_server.test.ssl_enforcement_enabled } @@ -553,12 +560,12 @@ provider "azurerm" { } resource "azurerm_resource_group" "test" { - name = "acctestRG-psql-%d" - location = "%s" + name = "acctestRG-psql-%[1]d" + location = "%[2]s" } resource "azurerm_postgresql_server" "test" { - name = "acctest-psql-server-%d" + name = "acctest-psql-server-%[1]d" location = azurerm_resource_group.test.location resource_group_name = azurerm_resource_group.test.name @@ -566,7 +573,7 @@ resource "azurerm_postgresql_server" "test" { administrator_login_password = "H@Sh1CoR3!updated" sku_name = "GP_Gen5_4" - version = "%s" + version = "%[3]s" storage_mb = 640000 backup_retention_days = 7 @@ -577,8 +584,72 @@ resource "azurerm_postgresql_server" "test" { public_network_access_enabled = false ssl_enforcement_enabled = true ssl_minimal_tls_version_enforced = "TLS1_2" + + threat_detection_policy { + enabled = true + disabled_alerts = ["Sql_Injection", "Data_Exfiltration"] + email_account_admins = true + email_addresses = ["kt@example.com", "admin@example.com"] + + retention_days = 7 + } } -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, version) +`, data.RandomInteger, data.Locations.Primary, version) +} + +func testAccAzureRMPostgreSQLServer_complete2(data acceptance.TestData, version string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-psql-%[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_postgresql_server" "test" { + name = "acctest-psql-server-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!updated" + + sku_name = "GP_Gen5_4" + version = "%[3]s" + storage_mb = 640000 + + backup_retention_days = 14 + geo_redundant_backup_enabled = false + auto_grow_enabled = false + + infrastructure_encryption_enabled = false + public_network_access_enabled = true + ssl_enforcement_enabled = false + ssl_minimal_tls_version_enforced = "TLS1_1" + + threat_detection_policy { + enabled = true + disabled_alerts = ["Sql_Injection"] + email_account_admins = true + email_addresses = ["kt@example.com"] + + 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 testAccAzureRMPostgreSQLServer_sku(data acceptance.TestData, version, sku string) string { diff --git a/website/docs/r/postgresql_server.html.markdown b/website/docs/r/postgresql_server.html.markdown index 7aa92293383d..25fe6491314c 100644 --- a/website/docs/r/postgresql_server.html.markdown +++ b/website/docs/r/postgresql_server.html.markdown @@ -87,8 +87,29 @@ The following arguments are supported: * `storage_mb` - (Optional) Max storage allowed for a server. Possible values are between `5120` MB(5GB) and `1048576` MB(1TB) for the Basic SKU and between `5120` MB(5GB) and `4194304` MB(4TB) for General Purpose/Memory Optimized SKUs. For more information see the [product documentation](https://docs.microsoft.com/en-us/rest/api/postgresql/servers/create#StorageProfile). +* `threat_detection_policy` - (Optional) Threat detection policy configuration, known in the API as Server Security Alerts Policy. The `threat_detection_policy` block supports fields documented below. + * `tags` - (Optional) A mapping of tags to assign to the resource. +--- + +a `threat_detection_policy` block supports the following: + +* `enabled` - (Required) Is the policy enabled? + +* `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. + +* `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. + + ## Attributes Reference The following attributes are exported: