Skip to content

Commit

Permalink
Add support for WAF packages (#475)
Browse files Browse the repository at this point in the history
Adds new resource for `cloudflare_waf_package` to control WAF rule packages
  • Loading branch information
xaf authored and jacobbednarz committed Oct 9, 2019
1 parent 8c202b9 commit cdaa76c
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 0 deletions.
1 change: 1 addition & 0 deletions cloudflare/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func Provider() terraform.ResourceProvider {
"cloudflare_rate_limit": resourceCloudflareRateLimit(),
"cloudflare_record": resourceCloudflareRecord(),
"cloudflare_spectrum_application": resourceCloudflareSpectrumApplication(),
"cloudflare_waf_package": resourceCloudflareWAFPackage(),
"cloudflare_waf_rule": resourceCloudflareWAFRule(),
"cloudflare_worker_route": resourceCloudflareWorkerRoute(),
"cloudflare_worker_script": resourceCloudflareWorkerScript(),
Expand Down
187 changes: 187 additions & 0 deletions cloudflare/resource_cloudflare_waf_package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package cloudflare

import (
"fmt"
"strings"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
)

func resourceCloudflareWAFPackage() *schema.Resource {
return &schema.Resource{
Create: resourceCloudflareWAFPackageCreate,
Read: resourceCloudflareWAFPackageRead,
Update: resourceCloudflareWAFPackageUpdate,
Delete: resourceCloudflareWAFPackageDelete,

Importer: &schema.ResourceImporter{
State: resourceCloudflareWAFPackageImport,
},

SchemaVersion: 0,
Schema: map[string]*schema.Schema{
"package_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"zone_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"sensitivity": {
Type: schema.TypeString,
Optional: true,
Default: "high",
ValidateFunc: validation.StringInSlice([]string{"high", "medium", "low", "off"}, false),
},

"action_mode": {
Type: schema.TypeString,
Optional: true,
Default: "challenge",
ValidateFunc: validation.StringInSlice([]string{"simulate", "block", "challenge"}, false),
},
},
}
}

func resourceCloudflareWAFPackageRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)

packageID := d.Get("package_id").(string)
zoneID := d.Get("zone_id").(string)

pkg, err := client.WAFPackage(zoneID, packageID)
if err != nil {
return (err)
}

d.Set("sensitivity", pkg.Sensitivity)
d.Set("action_mode", pkg.ActionMode)
d.SetId(pkg.ID)

return nil
}

func resourceCloudflareWAFPackageCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)

packageID := d.Get("package_id").(string)
zoneID := d.Get("zone_id").(string)
sensitivity := d.Get("sensitivity").(string)
actionMode := d.Get("action_mode").(string)

pkg, err := client.WAFPackage(zoneID, packageID)
if err != nil {
return fmt.Errorf("Unable to find WAF Package %s", packageID)
}

d.Set("zone_id", zoneID)
d.Set("package_id", packageID)
d.Set("sensitivity", sensitivity)
d.Set("action_mode", actionMode)

// Set the ID to the package_id parameter passed in from the user.
// All WAF packages already exist so we already know the package_id.
//
// This is a work around as we are not really "creating" a WAF Package,
// only associating it with our terraform config for future updates.
d.SetId(packageID)

if pkg.Sensitivity != sensitivity || pkg.ActionMode != actionMode {
err = resourceCloudflareWAFPackageUpdate(d, meta)
if err != nil {
d.SetId("")
return err
}
}

return nil
}

func resourceCloudflareWAFPackageDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)

packageID := d.Get("package_id").(string)
zoneID := d.Get("zone_id").(string)

pkg, err := client.WAFPackage(zoneID, packageID)
if err != nil {
return err
}

// Can't delete WAF Package so instead reset it to default
schema := resourceCloudflareWAFPackage().Schema
defaultSensitivity := schema["sensitivity"].Default.(string)
defaultActionMode := schema["action_mode"].Default.(string)

if pkg.Sensitivity != defaultSensitivity || pkg.ActionMode != defaultActionMode {
options := cloudflare.WAFPackageOptions{
Sensitivity: defaultSensitivity,
ActionMode: defaultActionMode,
}

_, err = client.UpdateWAFPackage(zoneID, packageID, options)
if err != nil {
return err
}
}

return nil
}

func resourceCloudflareWAFPackageUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)

packageID := d.Get("package_id").(string)
zoneID := d.Get("zone_id").(string)
sensitivity := d.Get("sensitivity").(string)
actionMode := d.Get("action_mode").(string)

options := cloudflare.WAFPackageOptions{
Sensitivity: sensitivity,
ActionMode: actionMode,
}

_, err := client.UpdateWAFPackage(zoneID, packageID, options)
if err != nil {
return err
}

return nil
}

func resourceCloudflareWAFPackageImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*cloudflare.API)

// split the id so we can lookup
idAttr := strings.SplitN(d.Id(), "/", 2)
var zoneID string
var packageID string
if len(idAttr) == 2 {
zoneID = idAttr[0]
packageID = idAttr[1]
} else {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"zoneID/PackageID\" for import", d.Id())
}

pkg, err := client.WAFPackage(zoneID, packageID)
if err != nil {
return nil, err
}

d.Set("package_id", pkg.ID)
d.Set("zone_id", zoneID)
d.Set("sensitivity", pkg.Sensitivity)
d.Set("action_mode", pkg.ActionMode)

d.SetId(pkg.ID)

return []*schema.ResourceData{d}, nil
}
109 changes: 109 additions & 0 deletions cloudflare/resource_cloudflare_waf_package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cloudflare

import (
"fmt"
"os"
"testing"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func TestAccCloudflareWAFPackage_CreateThenUpdate(t *testing.T) {
t.Parallel()
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
packageID, err := testAccGetWAFPackage(zoneID)
if err != nil {
t.Errorf(err.Error())
}

rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_waf_package.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareWAFPackageDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareWAFPackageConfig(zoneID, packageID, "medium", "simulate", rnd),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "package_id", packageID),
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "sensitivity", "medium"),
resource.TestCheckResourceAttr(name, "action_mode", "simulate"),
),
},
{
Config: testAccCheckCloudflareWAFPackageConfig(zoneID, packageID, "low", "block", rnd),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "package_id", packageID),
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "sensitivity", "low"),
resource.TestCheckResourceAttr(name, "action_mode", "block"),
),
},
},
})
}

func testAccGetWAFPackage(zoneID string) (string, error) {
if os.Getenv(resource.TestEnvVar) == "" {
// Test will be skipped as acceptance tests are not enabled,
// we thus don't need to use the client to grab a package ID
return "", nil
}

client, err := sharedClient()
if err != nil {
return "", err
}

pkgList, err := client.ListWAFPackages(zoneID)
if err != nil {
return "", fmt.Errorf("Error while listing WAF packages: %s", err)
}

for _, pkg := range pkgList {
if pkg.DetectionMode == "anomaly" {
return pkg.ID, nil
}
}

return "", fmt.Errorf("No anomaly package found")
}

func testAccCheckCloudflareWAFPackageDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*cloudflare.API)

for _, rs := range s.RootModule().Resources {
if rs.Type != "cloudflare_waf_package" {
continue
}

pkg, err := client.WAFPackage(rs.Primary.Attributes["zone_id"], rs.Primary.ID)
if err != nil {
return err
}

if pkg.Sensitivity != "high" {
return fmt.Errorf("Expected sensitivity to be reset to high, got: %s", pkg.Sensitivity)
}
if pkg.ActionMode != "challenge" {
return fmt.Errorf("Expected action_mode to be reset to challenge, got: %s", pkg.ActionMode)
}
}

return nil
}

func testAccCheckCloudflareWAFPackageConfig(zoneID, packageID, sensitivity, actionMode, name string) string {
return fmt.Sprintf(`
resource "cloudflare_waf_package" "%[5]s" {
zone_id = "%[1]s"
package_id = "%[2]s"
sensitivity = "%[3]s"
action_mode = "%[4]s"
}`, zoneID, packageID, sensitivity, actionMode, name)
}
46 changes: 46 additions & 0 deletions website/docs/r/waf_package.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
layout: "cloudflare"
page_title: "Cloudflare: cloudflare_waf_package"
sidebar_current: "docs-cloudflare-resource-waf-package"
description: |-
Provides a Cloudflare WAF rule package resource for a particular zone.
---

# cloudflare_waf_package

Provides a Cloudflare WAF rule package resource for a particular zone. This can be used to configure firewall behaviour for pre-defined firewall packages.

## Example Usage

```hcl
resource "cloudflare_waf_package" "owasp" {
package_id = "a25a9a7e9c00afc1fb2e0245519d725b"
zone_id = "ae36f999674d196762efcc5abb06b345"
sensitivity = "medium"
action_mode = "simulate"
}
```

## Argument Reference

The following arguments are supported:

* `zone_id` - (Required) The DNS zone ID to apply to.
* `package_id` - (Required) The WAF Package ID.
* `sensitivity` - (Optional) The sensitivity of the package, can be one of ["high", "medium", "low", "off"].
* `action_mode` - (Optional) The action mode of the package, can be one of ["block", "challenge", "simulate"].


## Attributes Reference

The following attributes are exported:

* `id` - The WAF Package ID, the same as package_id.

## Import

Packages can be imported using a composite ID formed of zone ID and the WAF Package ID, e.g.

```
$ terraform import cloudflare_waf_package.owasp ae36f999674d196762efcc5abb06b345/a25a9a7e9c00afc1fb2e0245519d725b
```

0 comments on commit cdaa76c

Please sign in to comment.