diff --git a/.changelog/2672.txt b/.changelog/2672.txt new file mode 100644 index 0000000000..dc06b0c2d7 --- /dev/null +++ b/.changelog/2672.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +introduce bot management resource +``` diff --git a/docs/resources/bot_management.md b/docs/resources/bot_management.md new file mode 100644 index 0000000000..87496cc557 --- /dev/null +++ b/docs/resources/bot_management.md @@ -0,0 +1,63 @@ +--- +page_title: "cloudflare_bot_management Resource - Cloudflare" +subcategory: "" +description: |- + Provides a resource to configure Bot Management. + Specifically, this resource can be used to manage: + Bot Fight ModeSuper Bot Fight ModeBot Management for Enterprise +--- + +# cloudflare_bot_management (Resource) + +Provides a resource to configure Bot Management. + +Specifically, this resource can be used to manage: + +- **Bot Fight Mode** +- **Super Bot Fight Mode** +- **Bot Management for Enterprise** + +## Example Usage + +```terraform +resource "cloudflare_bot_management" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + enable_js = true + sbfm_definitely_automated = "block" + sbfm_likely_automated = "managed_challenge" + sbfm_verified_bots = "allow" + sbfm_static_resource_protection = false + optimize_wordpress = true +} +``` + +## Schema + +### Required + +- `zone_id` (String) The zone identifier to target for the resource. **Modifying this attribute will force creation of a new resource.** + +### Optional + +- `auto_update_model` (Boolean) Automatically update to the newest bot detection models created by Cloudflare as they are released. [Learn more.](https://developers.cloudflare.com/bots/reference/machine-learning-models#model-versions-and-release-notes). +- `enable_js` (Boolean) Use lightweight, invisible JavaScript detections to improve Bot Management. [Learn more about JavaScript Detections](https://developers.cloudflare.com/bots/reference/javascript-detections/). +- `fight_mode` (Boolean) Whether to enable Bot Fight Mode. +- `optimize_wordpress` (Boolean) Whether to optimize Super Bot Fight Mode protections for Wordpress. +- `sbfm_definitely_automated` (String) Super Bot Fight Mode (SBFM) action to take on definitely automated requests. +- `sbfm_likely_automated` (String) Super Bot Fight Mode (SBFM) action to take on likely automated requests. +- `sbfm_static_resource_protection` (Boolean) Super Bot Fight Mode (SBFM) to enable static resource protection. Enable if static resources on your application need bot protection. Note: Static resource protection can also result in legitimate traffic being blocked. +- `sbfm_verified_bots` (String) Super Bot Fight Mode (SBFM) action to take on verified bots requests. +- `suppress_session_score` (Boolean) Whether to disable tracking the highest bot score for a session in the Bot Management cookie. + +### Read-Only + +- `id` (String) The ID of this resource. +- `using_latest_model` (Boolean) A read-only field that indicates whether the zone currently is running the latest ML model. + +## Import + +Import is supported using the following syntax: + +```shell +$ terraform import cloudflare_bot_management.example +``` diff --git a/examples/resources/cloudflare_bot_management/import.sh b/examples/resources/cloudflare_bot_management/import.sh new file mode 100644 index 0000000000..b1c195ebd4 --- /dev/null +++ b/examples/resources/cloudflare_bot_management/import.sh @@ -0,0 +1 @@ +$ terraform import cloudflare_bot_management.example diff --git a/examples/resources/cloudflare_bot_management/resource.tf b/examples/resources/cloudflare_bot_management/resource.tf new file mode 100644 index 0000000000..c289ed17d1 --- /dev/null +++ b/examples/resources/cloudflare_bot_management/resource.tf @@ -0,0 +1,9 @@ +resource "cloudflare_bot_management" "example" { + zone_id = "0da42c8d2132a9ddaf714f9e7c920711" + enable_js = true + sbfm_definitely_automated = "block" + sbfm_likely_automated = "managed_challenge" + sbfm_verified_bots = "allow" + sbfm_static_resource_protection = false + optimize_wordpress = true +} diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 1374dc0625..cdd789b8b9 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -241,6 +241,7 @@ func New(version string) func() *schema.Provider { "cloudflare_spectrum_application": resourceCloudflareSpectrumApplication(), "cloudflare_split_tunnel": resourceCloudflareSplitTunnel(), "cloudflare_static_route": resourceCloudflareStaticRoute(), + "cloudflare_bot_management": resourceCloudflareBotManagement(), "cloudflare_teams_account": resourceCloudflareTeamsAccount(), "cloudflare_teams_list": resourceCloudflareTeamsList(), "cloudflare_teams_location": resourceCloudflareTeamsLocation(), diff --git a/internal/sdkv2provider/resource_cloudflare_bot_management.go b/internal/sdkv2provider/resource_cloudflare_bot_management.go new file mode 100644 index 0000000000..83499aef72 --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_bot_management.go @@ -0,0 +1,155 @@ +package sdkv2provider + +import ( + "context" + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func resourceCloudflareBotManagement() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareBotManagementSchema(), + CreateContext: resourceCloudflareBotManagementCreate, + ReadContext: resourceCloudflareBotManagementRead, + UpdateContext: resourceCloudflareBotManagementUpdate, + DeleteContext: resourceCloudflareBotManagementDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareBotManagementImport, + }, + Description: heredoc.Doc(`Provides a resource to configure Bot Management. + + Specifically, this resource can be used to manage: + + - **Bot Fight Mode** + - **Super Bot Fight Mode** + - **Bot Management for Enterprise** + `), + } +} + +func resourceCloudflareBotManagementCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId(d.Get(consts.ZoneIDSchemaKey).(string)) + + return resourceCloudflareBotManagementUpdate(ctx, d, meta) +} + +func resourceCloudflareBotManagementRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + bm, err := client.GetBotManagement(ctx, cloudflare.ZoneIdentifier(d.Id())) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to fetch bot management configuration: %w", err)) + } + + if bm.EnableJS != nil { + d.Set("enable_js", bm.EnableJS) + } + + if bm.FightMode != nil { + d.Set("fight_mode", bm.FightMode) + } + + if bm.SBFMDefinitelyAutomated != nil { + d.Set("sbfm_definitely_automated", bm.SBFMDefinitelyAutomated) + } + + if bm.SBFMLikelyAutomated != nil { + d.Set("sbfm_likely_automated", bm.SBFMLikelyAutomated) + } + + if bm.SBFMVerifiedBots != nil { + d.Set("sbfm_verified_bots", bm.SBFMVerifiedBots) + } + + if bm.SBFMStaticResourceProtection != nil { + d.Set("sbfm_static_resource_protection", bm.SBFMStaticResourceProtection) + } + + if bm.OptimizeWordpress != nil { + d.Set("optimize_wordpress", bm.OptimizeWordpress) + } + + if bm.SuppressSessionScore != nil { + d.Set("suppress_session_score", bm.SuppressSessionScore) + } + + if bm.AutoUpdateModel != nil { + d.Set("auto_update_model", bm.AutoUpdateModel) + } + + if bm.UsingLatestModel != nil { + d.Set("using_latest_model", bm.UsingLatestModel) + } + + return nil +} + +func resourceCloudflareBotManagementUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get(consts.ZoneIDSchemaKey).(string) + + params := buildBotManagementParams(d) + + _, err := client.UpdateBotManagement(ctx, cloudflare.ZoneIdentifier(zoneID), params) + if err != nil { + return diag.FromErr(errors.Wrap(err, "failed to update bot management configuration")) + } + + return resourceCloudflareBotManagementRead(ctx, d, meta) +} + +// Deletion of bot management configuration is not something we support, we will +// use a dummy handler for now. +func resourceCloudflareBotManagementDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceCloudflareBotManagementImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.SetId(d.Id()) + d.Set(consts.ZoneIDSchemaKey, d.Id()) + + resourceCloudflareBotManagementRead(ctx, d, meta) + + return []*schema.ResourceData{d}, nil +} + +func buildBotManagementParams(d *schema.ResourceData) cloudflare.UpdateBotManagementParams { + bm := cloudflare.UpdateBotManagementParams{} + + if val, exists := d.GetOkExists("enable_js"); exists { + bm.EnableJS = cloudflare.BoolPtr(val.(bool)) + } + if val, exists := d.GetOkExists("fight_mode"); exists { + bm.EnableJS = cloudflare.BoolPtr(val.(bool)) + } + + if val, exists := d.GetOkExists("sbfm_definitely_automated"); exists { + bm.SBFMDefinitelyAutomated = cloudflare.StringPtr(val.(string)) + } + if val, exists := d.GetOkExists("sbfm_likely_automated"); exists { + bm.SBFMLikelyAutomated = cloudflare.StringPtr(val.(string)) + } + if val, exists := d.GetOkExists("sbfm_verified_bots"); exists { + bm.SBFMVerifiedBots = cloudflare.StringPtr(val.(string)) + } + if val, exists := d.GetOkExists("sbfm_static_resource_protection"); exists { + bm.SBFMStaticResourceProtection = cloudflare.BoolPtr(val.(bool)) + } + if val, exists := d.GetOkExists("optimize_wordpress"); exists { + bm.OptimizeWordpress = cloudflare.BoolPtr(val.(bool)) + } + + if val, exists := d.GetOkExists("suppress_session_score"); exists { + bm.SuppressSessionScore = cloudflare.BoolPtr(val.(bool)) + } + if val, exists := d.GetOkExists("auto_update_model"); exists { + bm.AutoUpdateModel = cloudflare.BoolPtr(val.(bool)) + } + + return bm +} diff --git a/internal/sdkv2provider/resource_cloudflare_bot_management_test.go b/internal/sdkv2provider/resource_cloudflare_bot_management_test.go new file mode 100644 index 0000000000..6cc36ae3c5 --- /dev/null +++ b/internal/sdkv2provider/resource_cloudflare_bot_management_test.go @@ -0,0 +1,113 @@ +package sdkv2provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCloudflareBotManagement_SBFM(t *testing.T) { + rnd := generateRandomResourceName() + resourceID := "cloudflare_bot_management." + rnd + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + + sbfmConfig := cloudflare.BotManagement{ + EnableJS: cloudflare.BoolPtr(true), + SBFMDefinitelyAutomated: cloudflare.StringPtr("managed_challenge"), + SBFMLikelyAutomated: cloudflare.StringPtr("block"), + SBFMVerifiedBots: cloudflare.StringPtr("allow"), + SBFMStaticResourceProtection: cloudflare.BoolPtr(false), + OptimizeWordpress: cloudflare.BoolPtr(true), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareBotManagementSBFM(rnd, zoneID, sbfmConfig), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceID, consts.ZoneIDSchemaKey, zoneID), + resource.TestCheckResourceAttr(resourceID, "enable_js", "true"), + resource.TestCheckResourceAttr(resourceID, "sbfm_definitely_automated", "managed_challenge"), + resource.TestCheckResourceAttr(resourceID, "sbfm_likely_automated", "block"), + resource.TestCheckResourceAttr(resourceID, "sbfm_verified_bots", "allow"), + resource.TestCheckResourceAttr(resourceID, "sbfm_static_resource_protection", "false"), + resource.TestCheckResourceAttr(resourceID, "optimize_wordpress", "true"), + ), + }, + { + ResourceName: resourceID, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccCloudflareBotManagement_Unentitled(t *testing.T) { + rnd := generateRandomResourceName() + resourceID := "cloudflare_bot_management." + rnd + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + + bmEntConfig := cloudflare.BotManagement{ + EnableJS: cloudflare.BoolPtr(true), + SuppressSessionScore: cloudflare.BoolPtr(false), + AutoUpdateModel: cloudflare.BoolPtr(false), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testCloudflareBotManagementEntSubscription(rnd, zoneID, bmEntConfig), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceID, consts.ZoneIDSchemaKey, zoneID), + resource.TestCheckResourceAttr(resourceID, "enable_js", "true"), + resource.TestCheckResourceAttr(resourceID, "suppress_session_score", "false"), + resource.TestCheckResourceAttr(resourceID, "auto_update_model", "false"), + ), + ExpectError: regexp.MustCompile("zone not entitled to disable"), + }, + }, + }) +} + +func testCloudflareBotManagementSBFM(resourceName, rnd string, bm cloudflare.BotManagement) string { + return fmt.Sprintf(` + resource "cloudflare_bot_management" "%[1]s" { + zone_id = "%[2]s" + + enable_js = "%[3]t" + + sbfm_definitely_automated = "%[4]s" + sbfm_likely_automated = "%[5]s" + sbfm_verified_bots = "%[6]s" + sbfm_static_resource_protection = "%[7]t" + optimize_wordpress = "%[8]t" + } +`, resourceName, rnd, + *bm.EnableJS, *bm.SBFMDefinitelyAutomated, + *bm.SBFMLikelyAutomated, *bm.SBFMVerifiedBots, + *bm.SBFMStaticResourceProtection, *bm.OptimizeWordpress) +} + +func testCloudflareBotManagementEntSubscription(resourceName, rnd string, bm cloudflare.BotManagement) string { + return fmt.Sprintf(` + resource "cloudflare_bot_management" "%[1]s" { + zone_id = "%[2]s" + + enable_js = "%[3]t" + + suppress_session_score = "%[4]t" + auto_update_model = "%[5]t" + } +`, resourceName, rnd, + *bm.EnableJS, *bm.SuppressSessionScore, *bm.AutoUpdateModel) +} diff --git a/internal/sdkv2provider/schema_cloudflare_bot_management.go b/internal/sdkv2provider/schema_cloudflare_bot_management.go new file mode 100644 index 0000000000..978203b45a --- /dev/null +++ b/internal/sdkv2provider/schema_cloudflare_bot_management.go @@ -0,0 +1,67 @@ +package sdkv2provider + +import ( + "github.com/cloudflare/terraform-provider-cloudflare/internal/consts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareBotManagementSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + consts.ZoneIDSchemaKey: { + Description: consts.ZoneIDSchemaDescription, + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "enable_js": { + Type: schema.TypeBool, + Optional: true, + Description: "Use lightweight, invisible JavaScript detections to improve Bot Management. [Learn more about JavaScript Detections](https://developers.cloudflare.com/bots/reference/javascript-detections/).", + }, + "fight_mode": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to enable Bot Fight Mode.", + }, + "sbfm_definitely_automated": { + Type: schema.TypeString, + Optional: true, + Description: "Super Bot Fight Mode (SBFM) action to take on definitely automated requests.", + }, + "sbfm_likely_automated": { + Type: schema.TypeString, + Optional: true, + Description: " Super Bot Fight Mode (SBFM) action to take on likely automated requests.", + }, + "sbfm_verified_bots": { + Type: schema.TypeString, + Optional: true, + Description: "Super Bot Fight Mode (SBFM) action to take on verified bots requests.", + }, + "sbfm_static_resource_protection": { + Type: schema.TypeBool, + Optional: true, + Description: "Super Bot Fight Mode (SBFM) to enable static resource protection. Enable if static resources on your application need bot protection. Note: Static resource protection can also result in legitimate traffic being blocked.", + }, + "optimize_wordpress": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to optimize Super Bot Fight Mode protections for Wordpress.", + }, + "suppress_session_score": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to disable tracking the highest bot score for a session in the Bot Management cookie.", + }, + "auto_update_model": { + Type: schema.TypeBool, + Optional: true, + Description: "Automatically update to the newest bot detection models created by Cloudflare as they are released. [Learn more.](https://developers.cloudflare.com/bots/reference/machine-learning-models#model-versions-and-release-notes)", + }, + "using_latest_model": { + Type: schema.TypeBool, + Computed: true, + Description: "A read-only field that indicates whether the zone currently is running the latest ML model.", + }, + } +}