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.
  • Loading branch information
beandrad committed Sep 30, 2020
1 parent 2bb8950 commit ba2af7e
Show file tree
Hide file tree
Showing 60 changed files with 36,651 additions and 47 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
@@ -0,0 +1,30 @@
package parse

import (
"fmt"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
)

type SecurityCenterSubscriptionPricingId struct {
ResourceType string
}

func SecurityCenterSubscriptionPricingID(input string) (*SecurityCenterSubscriptionPricingId, error) {
id, err := azure.ParseAzureResourceID(input)
if err != nil {
return nil, fmt.Errorf("unable to parse Security Center Subscription Pricing ID %q: %+v", input, err)
}

pricing := SecurityCenterSubscriptionPricingId{}

if pricing.ResourceType, err = id.PopSegment("pricings"); err != nil {
return nil, err
}

if err := id.ValidateNoEmptySegments(input); err != nil {
return nil, err
}

return &pricing, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package parse

import (
"testing"
)

func TestSecurityCenterSubscriptionPricingID(t *testing.T) {
testData := []struct {
ResourceType string
Input string
Error bool
Expect *SecurityCenterSubscriptionPricingId
}{
{
ResourceType: "Empty",
Input: "",
Error: true,
},
{
ResourceType: "No Pricings Segment",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000",
Error: true,
},
{
ResourceType: "No Pricings Value",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/pricings/",
Error: true,
},
{
ResourceType: "Security Center Subscription Pricing ID",
Input: "/subscriptions/00000000-0000-0000-0000-000000000000/pricings/VirtualMachines",
Expect: &SecurityCenterSubscriptionPricingId{
ResourceType: "VirtualMachines",
},
},
}

for _, v := range testData {
t.Logf("[DEBUG] Testing %q", v.ResourceType)

actual, err := SecurityCenterSubscriptionPricingID(v.Input)
if err != nil {
if v.Error {
continue
}

t.Fatalf("Expected a value but got an error: %s", err)
}

if actual.ResourceType != v.Expect.ResourceType {
t.Fatalf("Expected %q but got %q for ResourceType", v.Expect.ResourceType, actual.ResourceType)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,27 @@ import (
"log"
"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/services/securitycenter/parse"
azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema"
"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,
Read: resourceArmSecurityCenterSubscriptionPricingRead,
Update: resourceArmSecurityCenterSubscriptionPricingUpdate,
Delete: resourceArmSecurityCenterSubscriptionPricingDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parse.SecurityCenterSubscriptionPricingID(id)
return err
}),

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(60 * time.Minute),
Expand All @@ -35,6 +34,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,6 +52,20 @@ 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),
},
},
}
}
Expand All @@ -53,8 +75,6 @@ func resourceArmSecurityCenterSubscriptionPricingUpdate(d *schema.ResourceData,
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 +84,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,20 +108,26 @@ func resourceArmSecurityCenterSubscriptionPricingRead(d *schema.ResourceData, me
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

resp, err := client.GetSubscriptionPricing(ctx, securityCenterSubscriptionPricingName)
id, err := parse.SecurityCenterSubscriptionPricingID(d.Id())
if err != nil {
return err
}

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

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

if properties := resp.PricingProperties; properties != nil {
d.Set("tier", properties.PricingTier)
}
d.Set("resource_type", id.ResourceType)

return 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
Loading

0 comments on commit ba2af7e

Please sign in to comment.