Skip to content

Commit

Permalink
Allow to set security center pricing tier for a particular resource type
Browse files Browse the repository at this point in the history
At the moment, only Virtual Machines are set with the security center
standard or free pricing tiers when using `azurerm_security_center_subscription_pricing`.
This change adds the field `resource_type`, which allows to specify the
resource type for which we want to update the pricing tier.

The v1 security center pricing client only allows to get the pricing
tier of the default resource type (Virtual Machines) to check whether
the subscription has standard or free security center pricing tier.
However, a partial standard pricing tier (where one or more
resource types have standar tier enabled) allows for a security center
workspace to be created.

This commit changes the client to v3 and checks if any resource type
in the subscription has standard pricing tier enabled,
and if so, it allows the creation of a security center workspace.

provider
  • Loading branch information
beandrad committed Sep 24, 2020
1 parent af53259 commit a207786
Show file tree
Hide file tree
Showing 57 changed files with 36,569 additions and 45 deletions.
19 changes: 10 additions & 9 deletions azurerm/internal/services/securitycenter/client/client.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
package client

import (
"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v1.0/security"
securityv1 "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v1.0/security"
securityv3 "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/common"
)

type Client struct {
ContactsClient *security.ContactsClient
PricingClient *security.PricingsClient
WorkspaceClient *security.WorkspaceSettingsClient
AdvancedThreatProtectionClient *security.AdvancedThreatProtectionClient
ContactsClient *securityv1.ContactsClient
PricingClient *securityv3.PricingsClient
WorkspaceClient *securityv1.WorkspaceSettingsClient
AdvancedThreatProtectionClient *securityv1.AdvancedThreatProtectionClient
}

func NewClient(o *common.ClientOptions) *Client {
ascLocation := "Global"

ContactsClient := security.NewContactsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
ContactsClient := securityv1.NewContactsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&ContactsClient.Client, o.ResourceManagerAuthorizer)

PricingClient := security.NewPricingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
PricingClient := securityv3.NewPricingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&PricingClient.Client, o.ResourceManagerAuthorizer)

WorkspaceClient := security.NewWorkspaceSettingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
WorkspaceClient := securityv1.NewWorkspaceSettingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&WorkspaceClient.Client, o.ResourceManagerAuthorizer)

AdvancedThreatProtectionClient := security.NewAdvancedThreatProtectionClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
AdvancedThreatProtectionClient := securityv1.NewAdvancedThreatProtectionClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&AdvancedThreatProtectionClient.Client, o.ResourceManagerAuthorizer)

return &Client{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@ package securitycenter
import (
"fmt"
"log"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v1.0/security"
"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

// NOTE: seems default is the only valid pricing name:
// Code="InvalidInputJson" Message="Pricing name 'kt's price' is not allowed. Expected 'default' for this scope."
const securityCenterSubscriptionPricingName = "default"

func resourceArmSecurityCenterSubscriptionPricing() *schema.Resource {
return &schema.Resource{
Create: resourceArmSecurityCenterSubscriptionPricingUpdate,
Expand All @@ -25,7 +22,15 @@ func resourceArmSecurityCenterSubscriptionPricing() *schema.Resource {
Delete: resourceArmSecurityCenterSubscriptionPricingDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
subscription_id, resource_type, err := resourceArmSecurityCenterSubscriptionPricingParseId(d.Id())
if err != nil {
return nil, err
}
d.Set("resource_type", resource_type)
d.SetId(fmt.Sprintf("/subscriptions/%v/providers/Microsoft.Security/pricings/%v", subscription_id, resource_type))
return []*schema.ResourceData{d}, nil
},
},

Timeouts: &schema.ResourceTimeout{
Expand All @@ -35,6 +40,15 @@ func resourceArmSecurityCenterSubscriptionPricing() *schema.Resource {
Delete: schema.DefaultTimeout(60 * time.Minute),
},

SchemaVersion: 1,
StateUpgraders: []schema.StateUpgrader{
{
Type: ResourceArmSecurityCenterSubscriptionPricingV0().CoreConfigSchema().ImpliedType(),
Upgrade: ResourceArmSecurityCenterSubscriptionPricingUpgradeV0ToV1,
Version: 0,
},
},

Schema: map[string]*schema.Schema{
"tier": {
Type: schema.TypeString,
Expand All @@ -44,17 +58,39 @@ func resourceArmSecurityCenterSubscriptionPricing() *schema.Resource {
string(security.Standard),
}, false),
},
"resource_type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
"AppServices",
"ContainerRegistry",
"KeyVaults",
"KubernetesService",
"SqlServers",
"SqlServerVirtualMachines",
"StorageAccounts",
"VirtualMachines",
}, false),
},
},
}
}

func resourceArmSecurityCenterSubscriptionPricingParseId(id string) (string, string, error) {
parts := strings.SplitN(id, "/", 7)

if len(parts) != 7 || parts[2] == "" || parts[6] == "" {
return "", "", fmt.Errorf("unexpected format of ID (%s)", id)
}

return parts[2], parts[6], nil
}

func resourceArmSecurityCenterSubscriptionPricingUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).SecurityCenter.PricingClient
ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

name := securityCenterSubscriptionPricingName

// not doing import check as afaik it always exists (cannot be deleted)
// all this resource does is flip a boolean

Expand All @@ -64,11 +100,13 @@ func resourceArmSecurityCenterSubscriptionPricingUpdate(d *schema.ResourceData,
},
}

if _, err := client.UpdateSubscriptionPricing(ctx, name, pricing); err != nil {
resource_type := d.Get("resource_type").(string)

if _, err := client.Update(ctx, resource_type, pricing); err != nil {
return fmt.Errorf("Error creating/updating Security Center Subscription pricing: %+v", err)
}

resp, err := client.GetSubscriptionPricing(ctx, name)
resp, err := client.Get(ctx, resource_type)
if err != nil {
return fmt.Errorf("Error reading Security Center Subscription pricing: %+v", err)
}
Expand All @@ -86,15 +124,17 @@ func resourceArmSecurityCenterSubscriptionPricingRead(d *schema.ResourceData, me
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

resp, err := client.GetSubscriptionPricing(ctx, securityCenterSubscriptionPricingName)
resource_type := d.Get("resource_type").(string)

resp, err := client.Get(ctx, resource_type)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[DEBUG] Security Center Subscription was not found: %v", err)
log.Printf("[DEBUG] %v Security Center Subscription was not found: %v", resource_type, err)
d.SetId("")
return nil
}

return fmt.Errorf("Error reading Security Center Subscription pricing: %+v", err)
return fmt.Errorf("Error reading %v Security Center Subscription pricing: %+v", resource_type, err)
}

if properties := resp.PricingProperties; properties != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package securitycenter

import (
"log"

"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
)

func ResourceArmSecurityCenterSubscriptionPricingV0() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"tier": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string(security.Free),
string(security.Standard),
}, false),
},
},
}
}

func ResourceArmSecurityCenterSubscriptionPricingUpgradeV0ToV1(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
log.Println("[DEBUG] Migrating ResourceType from v0 to v1 format")

rawState["resource_type"] = "VirtualMachines"

return rawState, nil
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package securitycenter

import (
"context"
"fmt"
"log"
"time"

"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v1.0/security"
securityv1 "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v1.0/security"
securityv3 "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
Expand Down Expand Up @@ -77,20 +79,18 @@ func resourceArmSecurityCenterWorkspaceCreateUpdate(d *schema.ResourceData, meta

// get pricing tier, workspace can only be configured when tier is not Free.
// API does not error, it just doesn't set the workspace scope
price, err := priceClient.GetSubscriptionPricing(ctx, securityCenterSubscriptionPricingName)
isPricingStandard, err := isPricingStandard(ctx, priceClient)

if err != nil {
return fmt.Errorf("Error reading Security Center Subscription pricing: %+v", err)
return fmt.Errorf("Error checking Security Center Subscription pricing tier %v", err)
}

if price.PricingProperties == nil {
return fmt.Errorf("Security Center Subscription pricing propertier is nil")
}
if price.PricingProperties.PricingTier == security.Free {
if !isPricingStandard {
return fmt.Errorf("Security Center Subscription workspace cannot be set when pricing tier is `Free`")
}

contact := security.WorkspaceSetting{
WorkspaceSettingProperties: &security.WorkspaceSettingProperties{
contact := securityv1.WorkspaceSetting{
WorkspaceSettingProperties: &securityv1.WorkspaceSettingProperties{
Scope: utils.String(d.Get("scope").(string)),
WorkspaceID: utils.String(d.Get("workspace_id").(string)),
},
Expand Down Expand Up @@ -137,12 +137,33 @@ func resourceArmSecurityCenterWorkspaceCreateUpdate(d *schema.ResourceData, meta
}

if d.IsNewResource() {
d.SetId(*resp.(security.WorkspaceSetting).ID)
d.SetId(*resp.(securityv1.WorkspaceSetting).ID)
}

return resourceArmSecurityCenterWorkspaceRead(d, meta)
}

func isPricingStandard(ctx context.Context, priceClient *securityv3.PricingsClient) (bool, error) {
prices, err := priceClient.List(ctx)
if err != nil {
return false, fmt.Errorf("Error listing Security Center Subscription pricing: %+v", err)
}

if prices.Value != nil {
for _, resourcePrice := range *prices.Value {
if resourcePrice.PricingProperties == nil {
return false, fmt.Errorf("%v Security Center Subscription pricing propertier is nil", *resourcePrice.Type)
}

if resourcePrice.PricingProperties.PricingTier == securityv3.Standard {
return true, nil
}
}
}

return false, nil
}

func resourceArmSecurityCenterWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).SecurityCenter.WorkspaceClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package tests

import (
"testing"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/securitycenter"
)

func TestAzureRMSecurityCenterSubscriptionPricingMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
InputAttributes map[string]interface{}
ExpectedResourceType string
}{
"subscription_scope": {
StateVersion: 0,
InputAttributes: map[string]interface{}{
"tier": "Free",
},
ExpectedResourceType: "VirtualMachines",
},
"managementGroup_scope": {
StateVersion: 0,
InputAttributes: map[string]interface{}{
"tier": "Standard",
},
ExpectedResourceType: "VirtualMachines",
},
}

for _, tc := range cases {
rawState, _ := securitycenter.ResourceArmSecurityCenterSubscriptionPricingUpgradeV0ToV1(tc.InputAttributes, nil)

if rawState["resource_type"].(string) != tc.ExpectedResourceType {
t.Fatalf("ResourceType migration failed, expected %q, got: %q", tc.ExpectedResourceType, rawState["resource_type"].(string))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ func testAccAzureRMSecurityCenterSubscriptionPricing_update(t *testing.T) {
Providers: acceptance.SupportedProviders,
Steps: []resource.TestStep{
{
Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Standard"),
Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Standard", "AppServices"),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMSecurityCenterSubscriptionPricingExists(data.ResourceName),
resource.TestCheckResourceAttr(data.ResourceName, "tier", "Standard"),
),
},
data.ImportStep(),
{
Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Free"),
Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Free", "AppServices"),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMSecurityCenterSubscriptionPricingExists(data.ResourceName),
resource.TestCheckResourceAttr(data.ResourceName, "tier", "Free"),
Expand All @@ -51,27 +51,28 @@ func testCheckAzureRMSecurityCenterSubscriptionPricingExists(resourceName string

pricingName := rs.Primary.Attributes["pricings"]

resp, err := client.GetSubscriptionPricing(ctx, pricingName)
resp, err := client.Get(ctx, pricingName)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
return fmt.Errorf("Security Center Subscription Pricing %q was not found: %+v", pricingName, err)
}

return fmt.Errorf("Bad: GetSubscriptionPricing: %+v", err)
return fmt.Errorf("Bad: Get: %+v", err)
}

return nil
}
}

func testAccAzureRMSecurityCenterSubscriptionPricing_tier(tier string) string {
func testAccAzureRMSecurityCenterSubscriptionPricing_tier(tier string, resource_type string) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_security_center_subscription_pricing" "test" {
tier = "%s"
tier = "%s"
resource_type = "%s"
}
`, tier)
`, tier, resource_type)
}
Loading

0 comments on commit a207786

Please sign in to comment.