-
Notifications
You must be signed in to change notification settings - Fork 632
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds new resource for `cloudflare_waf_package` to control WAF rule packages
- Loading branch information
1 parent
8c202b9
commit cdaa76c
Showing
4 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |