Skip to content

Commit

Permalink
Add support for turnstile / challenge widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
Cyb3r-Jak3 committed Apr 15, 2023
1 parent 1bd50b6 commit de84ba5
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/2380.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
resource/challenge_widget: add support for challenge widgets / Turnstile
```
2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/challenge_widget"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/rulesets"
"github.com/cloudflare/terraform-provider-cloudflare/internal/sdkv2provider"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
Expand Down Expand Up @@ -307,6 +308,7 @@ func (p *CloudflareProvider) Configure(ctx context.Context, req provider.Configu
func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
rulesets.NewResource,
challenge_widget.NewResource,
}
}

Expand Down
15 changes: 15 additions & 0 deletions internal/framework/service/challenge_widget/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package challenge_widget

import "github.com/hashicorp/terraform-plugin-framework/types"

type ChallengeWidgetModel struct {
AccountID types.String `tfsdk:"account_id"`
ID types.String `tfsdk:"id"`
Domains types.Set `tfsdk:"domains"`
Name types.String `tfsdk:"name"`
Secret types.String `tfsdk:"secret"`
Region types.String `tfsdk:"region"`
Mode types.String `tfsdk:"mode"`
BotFightMode types.Bool `tfsdk:"bot_fight_mode"`
OffLabel types.Bool `tfsdk:"off_label"`
}
188 changes: 188 additions & 0 deletions internal/framework/service/challenge_widget/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package challenge_widget

import (
"context"
"fmt"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/expanders"
"strings"

"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/flatteners"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &ChallengeWidgetResource{}
var _ resource.ResourceWithImportState = &ChallengeWidgetResource{}

func NewResource() resource.Resource {
return &ChallengeWidgetResource{}
}

// ChallengeWidgetResource defines the resource implementation for challenge widgets.
type ChallengeWidgetResource struct {
client *cloudflare.API
}

func (r *ChallengeWidgetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_challenge_widget"
}

func (r *ChallengeWidgetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*cloudflare.API)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *cloudflare.API, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *ChallengeWidgetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *ChallengeWidgetModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

widget := buildChallengeWidgetFromModel(data)

createWidget, err := r.client.CreateChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()),
cloudflare.CreateChallengeWidgetRequest{
OffLabel: data.OffLabel.ValueBool(),
Name: widget.Name,
Domains: widget.Domains,
Mode: widget.Mode,
BotFightMode: widget.BotFightMode,
Region: widget.Region,
})
if err != nil {
resp.Diagnostics.AddError("Error creating challenge widget", err.Error())
}

data = buildChallengeModelFromWidget(
data.AccountID,
createWidget,
)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ChallengeWidgetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *ChallengeWidgetModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

widget, err := r.client.GetChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString())

if err != nil {
resp.Diagnostics.AddError("Error reading challenge widget", err.Error())
}

data = buildChallengeModelFromWidget(
data.AccountID,
widget,
)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ChallengeWidgetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *ChallengeWidgetModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

widget := buildChallengeWidgetFromModel(data)

updatedWidget, err := r.client.UpdateChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), widget)

if err != nil {
resp.Diagnostics.AddError("Error reading challenge widget", err.Error())
}

data = buildChallengeModelFromWidget(
data.AccountID,
updatedWidget,
)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ChallengeWidgetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *ChallengeWidgetModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

err := r.client.DeleteChallengeWidget(ctx, cloudflare.AccountIdentifier(data.AccountID.ValueString()), data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error deleting challenge widget", err.Error())
}
}

func (r *ChallengeWidgetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, "/")
if len(idParts) != 2 {
resp.Diagnostics.AddError("Error importing challenge widget", "Invalid ID specified. Please specify the ID as \"accounts_id/sitekey\"")
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("account_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...)
}

func buildChallengeWidgetFromModel(widget *ChallengeWidgetModel) cloudflare.ChallengeWidget {
built := cloudflare.ChallengeWidget{
SiteKey: widget.ID.ValueString(),
Name: widget.Name.ValueString(),
BotFightMode: widget.BotFightMode.ValueBool(),
Mode: widget.Mode.ValueString(),
Region: widget.Region.ValueString(),
Domains: expanders.StringSet(widget.Domains),
}

return built
}

func buildChallengeModelFromWidget(accountID types.String, widget cloudflare.ChallengeWidget) *ChallengeWidgetModel {
built := ChallengeWidgetModel{
AccountID: accountID,
ID: flatteners.String(widget.SiteKey),
Secret: flatteners.String(widget.Secret),
BotFightMode: types.BoolValue(widget.BotFightMode),
Name: flatteners.String(widget.Name),
Mode: flatteners.String(widget.Mode),
Region: flatteners.String(widget.Region),
}

var domains []attr.Value
for _, s := range widget.Domains {
domains = append(domains, types.StringValue(s))
}
built.Domains = flatteners.StringSet(domains)

return &built
}
54 changes: 54 additions & 0 deletions internal/framework/service/challenge_widget/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package challenge_widget_test

import (
"fmt"
"os"
"testing"

"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccCloudflareChallengeWidgetBasic(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_challenge_widget." + rnd

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareChallengeWidgetBasic(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "account_id", accountID),
resource.TestCheckResourceAttr(resourceName, "bot_fight_mode", "false"),
resource.TestCheckResourceAttr(resourceName, "domains.#", "1"),
resource.TestCheckResourceAttr(resourceName, "domains.0", "example.com"),
resource.TestCheckResourceAttr(resourceName, "mode", "invisible"),
resource.TestCheckResourceAttr(resourceName, "region", "world"),
),
},
{
ResourceName: resourceName,
ImportStateIdPrefix: fmt.Sprintf("%s/", accountID),
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccCheckCloudflareChallengeWidgetBasic(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_challenge_widget" "%[1]s" {
account_id = "%[2]s"
name = "%[1]s"
bot_fight_mode = false
domains = [ "example.com" ]
mode = "invisible"
region = "world"
}`, rnd, accountID)
}
69 changes: 69 additions & 0 deletions internal/framework/service/challenge_widget/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package challenge_widget

import (
"context"
"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func (r *ChallengeWidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: heredoc.Doc(`
The [Challenge Widget](https://developers.cloudflare.com/turnstile/) resource allows you to manage Cloudflare Turnstile Widgets.
`),

Attributes: map[string]schema.Attribute{
consts.IDSchemaKey: schema.StringAttribute{
Computed: true,
Optional: true,
MarkdownDescription: consts.IDSchemaDescription + " This is the site key value.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
consts.AccountIDSchemaKey: schema.StringAttribute{
MarkdownDescription: "The account identifier to target for the resource.",
Required: true,
},
"secret": schema.StringAttribute{
MarkdownDescription: "Secret key for this widget.",
Computed: true,
Sensitive: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "Human readable widget name.",
Required: true,
},
"domains": schema.SetAttribute{
MarkdownDescription: "Domains where the widget is deployed",
Required: true,
ElementType: types.StringType,
},
"mode": schema.StringAttribute{
MarkdownDescription: "Widget Mode",
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("non-interactive", "invisible", "managed"),
},
},
"region": schema.StringAttribute{
MarkdownDescription: "Region where this widget can be used.",
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("world"),
},
},
"bot_fight_mode": schema.BoolAttribute{
MarkdownDescription: "If bot_fight_mode is set to true, Cloudflare issues computationally expensive challenges in response to malicious bots (ENT only).",
Optional: true,
},
},
}
}

0 comments on commit de84ba5

Please sign in to comment.