diff --git a/.changelog/myPRnumber.txt b/.changelog/myPRnumber.txt new file mode 100644 index 00000000000..c15eaa00148 --- /dev/null +++ b/.changelog/myPRnumber.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/cloudflare_managed_headers: Add support of Managed Headers API. +``` \ No newline at end of file diff --git a/examples/resources/managed_headers/resource.tf b/examples/resources/managed_headers/resource.tf new file mode 100644 index 00000000000..63d43dd9c45 --- /dev/null +++ b/examples/resources/managed_headers/resource.tf @@ -0,0 +1,23 @@ +# Enable security headers using Managed Meaders +resource "cloudflare_managed_headers" "add_security_headers_example" { + zone_id = "cb029e245cfdd66dc8d2e570d5dd3322" + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } + + managed_response_headers { + id = "add_security_headers" + enabled = false + } + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } +} diff --git a/go.mod b/go.mod index 0abc5cd87a0..468740f47fe 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/bflad/tfproviderlint v0.28.1 github.com/client9/misspell v0.3.4 - github.com/cloudflare/cloudflare-go v0.41.0 + github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba github.com/fatih/color v1.13.0 // indirect github.com/golangci/golangci-lint v1.46.2 github.com/google/go-github v17.0.0+incompatible diff --git a/go.sum b/go.sum index 8c16b660061..0546d0cc7ca 100644 --- a/go.sum +++ b/go.sum @@ -192,8 +192,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.41.0 h1:4HiWuBpBj1fMiyWDfIlLGxnuPuEbkLi+SZWq9tgCFfc= -github.com/cloudflare/cloudflare-go v0.41.0/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE= +github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba h1:kfXdddUwNKsuqOHXJuGMnSY956aLmt9z1THZF8o95oI= +github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= diff --git a/internal/provider/resource_cloudflare_managed_headers.go b/internal/provider/resource_cloudflare_managed_headers.go new file mode 100644 index 00000000000..f201b900a8d --- /dev/null +++ b/internal/provider/resource_cloudflare_managed_headers.go @@ -0,0 +1,173 @@ +package provider + +import ( + "context" + "errors" + "fmt" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareManagedHeaders() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareManagedHeadersSchema(), + CreateContext: resourceCloudflareManagedHeadersCreate, + ReadContext: resourceCloudflareManagedHeadersRead, + UpdateContext: resourceCloudflareManagedHeadersUpdate, + DeleteContext: resourceCloudflareManagedHeadersDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareManagedHeadersImport, + }, + SchemaVersion: 0, + Description: ` +The [Cloudflare Managed Headers](https://developers.cloudflare.com/rules/transform/managed-transforms/) +allows you to add or remove some predefined headers to one's requests or origin responses. + +~> **NOTE:** You can configure Managed Headers using the dashboard (https://api.cloudflare.com/#managed-headers-api-properties) +Terraform will override yours configuration if it exists.`, + } +} + +func resourceCloudflareManagedHeadersCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + zoneID := d.Get("zone_id").(string) + d.SetId(zoneID) + return resourceCloudflareManagedHeadersUpdate(ctx, d, meta) +} + +func resourceCloudflareManagedHeadersRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + headers, err := client.ListZoneManagedHeaders(ctx, cloudflare.ListManagedHeadersParams{ + ZoneID: zoneID, + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error reading managed headers: %w", err)) + } + + if err := d.Set("managed_request_headers", buildResourceFromManagedHeaders(headers.ManagedRequestHeaders)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("managed_response_headers", buildResourceFromManagedHeaders(headers.ManagedResponseHeaders)); err != nil { + return diag.FromErr(err) + } + return nil +} + +func buildResourceFromManagedHeaders(headers []cloudflare.ManagedHeader) interface{} { + headersState := []map[string]interface{}{} + for _, header := range headers { + headersState = append(headersState, map[string]interface{}{ + "id": header.ID, + "enabled": header.Enabled, + }) + } + + return headersState +} + +func resourceCloudflareManagedHeadersUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + + mh, err := buildManagedHeadersFromResource(d) + if err != nil { + return diag.FromErr(fmt.Errorf("error building managed headers from resource: %w", err)) + } + if _, err := client.UpdateZoneManagedHeaders(ctx, cloudflare.UpdateManagedHeadersParams{ + ManagedHeaders: mh, + ZoneID: zoneID, + }); err != nil { + return diag.FromErr(fmt.Errorf("error updating managed headers: %w", err)) + } + return resourceCloudflareManagedHeadersRead(ctx, d, meta) +} + +// receives the resource config and builds a managed headers struct. +func buildManagedHeadersFromResource(d *schema.ResourceData) (cloudflare.ManagedHeaders, error) { + requestHeaders, ok := d.Get("managed_request_headers").([]interface{}) + if !ok { + return cloudflare.ManagedHeaders{}, errors.New("unable to create interface array type assertion") + } + reqHeaders, err := buildManagedHeadersListFromResource(requestHeaders) + if err != nil { + return cloudflare.ManagedHeaders{}, err + } + + responseHeaders, ok := d.Get("managed_response_headers").([]interface{}) + if !ok { + return cloudflare.ManagedHeaders{}, errors.New("unable to create interface array type assertion") + } + respHeaders, err := buildManagedHeadersListFromResource(responseHeaders) + if err != nil { + return cloudflare.ManagedHeaders{}, err + } + + return cloudflare.ManagedHeaders{ + ManagedRequestHeaders: reqHeaders, + ManagedResponseHeaders: respHeaders, + }, nil +} + +func buildManagedHeadersListFromResource(resource []interface{}) ([]cloudflare.ManagedHeader, error) { + headers := make([]cloudflare.ManagedHeader, 0, len(resource)) + for _, header := range resource { + h, ok := header.(map[string]interface{}) + if !ok { + return nil, errors.New("unable to create interface map type assertion for managed header") + } + id, ok := h["id"].(string) + if !ok { + return nil, errors.New("unable to create string type assertion for managed header ID") + } + enabled, ok := h["enabled"].(bool) + if !ok { + return nil, errors.New("unable to create bool type assertion for managed header enabled") + } + headers = append(headers, cloudflare.ManagedHeader{ + ID: id, + Enabled: enabled, + }) + } + return headers, nil +} + +func resourceCloudflareManagedHeadersDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + + headers, err := client.ListZoneManagedHeaders(ctx, cloudflare.ListManagedHeadersParams{ + ZoneID: zoneID, + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error reading managed headers: %w", err)) + } + + requestHeaders := make([]cloudflare.ManagedHeader, 0, len(headers.ManagedRequestHeaders)) + for _, header := range headers.ManagedRequestHeaders { + header.Enabled = false + requestHeaders = append(requestHeaders, header) + } + responseHeaders := make([]cloudflare.ManagedHeader, 0, len(headers.ManagedResponseHeaders)) + for _, header := range headers.ManagedResponseHeaders { + header.Enabled = false + responseHeaders = append(responseHeaders, header) + } + + if _, err := client.UpdateZoneManagedHeaders(ctx, cloudflare.UpdateManagedHeadersParams{ + ManagedHeaders: cloudflare.ManagedHeaders{ + ManagedRequestHeaders: requestHeaders, + ManagedResponseHeaders: responseHeaders, + }, + ZoneID: zoneID, + }); err != nil { + return diag.FromErr(fmt.Errorf("error deleting managed headers with ID %q: %w", d.Id(), err)) + } + + return nil +} + +func resourceCloudflareManagedHeadersImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + return nil, errors.New("Import is not yet supported for Managed Headers") +} diff --git a/internal/provider/resource_cloudflare_managed_headers_test.go b/internal/provider/resource_cloudflare_managed_headers_test.go new file mode 100644 index 00000000000..7f11560f7c6 --- /dev/null +++ b/internal/provider/resource_cloudflare_managed_headers_test.go @@ -0,0 +1,107 @@ +package provider + +import ( + "context" + "fmt" + "os" + "testing" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/pkg/errors" +) + +func init() { + resource.AddTestSweepers("cloudflare_managed_headers", &resource.Sweeper{ + Name: "cloudflare_managed_headers", + F: testSweepCloudflareManagedHeaders, + }) +} + +func testSweepCloudflareManagedHeaders(r string) error { + ctx := context.Background() + client, clientErr := sharedClient() + if clientErr != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr)) + } + + zone := os.Getenv("CLOUDFLARE_ZONE_ID") + if zone == "" { + return errors.New("CLOUDFLARE_ZONE_ID must be set") + } + + managedHeaders, err := client.ListZoneManagedHeaders(context.Background(), cloudflare.ListManagedHeadersParams{ + ZoneID: zoneID, + }) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to fetch Cloudflare Zone Managed Headers: %s", err)) + } + + requestHeaders := make([]cloudflare.ManagedHeader, 0, len(managedHeaders.ManagedRequestHeaders)) + for _, h := range managedHeaders.ManagedRequestHeaders { + tflog.Info(ctx, fmt.Sprintf("Disabling Cloudflare Zone Managed Header ID: %s", h.ID)) + h.Enabled = false + requestHeaders = append(requestHeaders, h) + } + responseHeaders := make([]cloudflare.ManagedHeader, 0, len(managedHeaders.ManagedResponseHeaders)) + for _, h := range managedHeaders.ManagedResponseHeaders { + tflog.Info(ctx, fmt.Sprintf("Disabling Cloudflare Zone Managed Header ID: %s", h.ID)) + h.Enabled = false + responseHeaders = append(responseHeaders, h) + } + + _, err = client.UpdateZoneManagedHeaders(context.Background(), cloudflare.UpdateManagedHeadersParams{ + ManagedHeaders: cloudflare.ManagedHeaders{ + ManagedRequestHeaders: requestHeaders, + ManagedResponseHeaders: responseHeaders, + }, + ZoneID: zoneID, + }) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to disable Cloudflare Zone Managed Headers: %s", err)) + } + + return nil +} + +func TestZoneCloudflareManagedHeaders(t *testing.T) { + t.Parallel() + + rnd := generateRandomResourceName() + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + resourceName := "cloudflare_managed_headers." + rnd + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareManagedHeaders(rnd, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "zone_id", zoneID), + resource.TestCheckResourceAttr(resourceName, "managed_request_headers.#", "1"), + resource.TestCheckResourceAttr(resourceName, "managed_request_headers.0.id", "add_true_client_ip_headers"), + resource.TestCheckResourceAttr(resourceName, "managed_request_headers.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "managed_response_headers.#", "1"), + resource.TestCheckResourceAttr(resourceName, "managed_response_headers.0.id", "add_security_headers"), + resource.TestCheckResourceAttr(resourceName, "managed_response_headers.0.enabled", "true"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareManagedHeaders(rnd, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_managed_headers" "%[1]s" { + zone_id = "%[2]s" + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + managed_response_headers { + id = "add_security_headers" + enabled = true + } + }`, rnd, zoneID) +} diff --git a/internal/provider/schema_cloudflare_managed_headers.go b/internal/provider/schema_cloudflare_managed_headers.go new file mode 100644 index 00000000000..0df7b3195c3 --- /dev/null +++ b/internal/provider/schema_cloudflare_managed_headers.go @@ -0,0 +1,51 @@ +package provider + +import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + +func resourceCloudflareManagedHeadersSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "zone_id": { + Description: "The zone identifier to target for the resource.", + Type: schema.TypeString, + Optional: false, + }, + "managed_request_headers": { + Description: "The list of managed request headers", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Unique headers rule identifier.", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether the headers rule is active.", + }, + }, + }, + }, + "managed_response_headers": { + Description: "The list of managed response headers", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Unique headers rule identifier.", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether the headers rule is active.", + }, + }, + }, + }, + } +}