Skip to content

Commit

Permalink
Merge pull request cloudflare#725 from jacobbednarz/access-cors-support
Browse files Browse the repository at this point in the history
resource/access_application: Add CORS support
  • Loading branch information
jacobbednarz authored Jul 12, 2020
2 parents d28b888 + 2cbf26d commit debf20e
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 1 deletion.
113 changes: 113 additions & 0 deletions cloudflare/resource_cloudflare_access_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,75 @@ func resourceCloudflareAccessApplication() *schema.Resource {
Default: "24h",
ValidateFunc: validation.StringInSlice([]string{"30m", "6h", "12h", "24h", "168h", "730h"}, false),
},
"cors_headers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_methods": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"allowed_origins": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"allowed_headers": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"allow_all_methods": {
Type: schema.TypeBool,
Optional: true,
},
"allow_all_origins": {
Type: schema.TypeBool,
Optional: true,
},
"allow_all_headers": {
Type: schema.TypeBool,
Optional: true,
},
"allow_credentials": {
Type: schema.TypeBool,
Optional: true,
},
"max_age": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(-1, 86400),
},
},
},
},
},
}
}

func resourceCloudflareAccessApplicationCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)

newAccessApplication := cloudflare.AccessApplication{
Name: d.Get("name").(string),
Domain: d.Get("domain").(string),
SessionDuration: d.Get("session_duration").(string),
}

if _, ok := d.GetOk("cors_headers"); ok {
CORSConfig, _ := convertCORSSchemaToStruct(d)
newAccessApplication.CorsHeaders = CORSConfig
}

log.Printf("[DEBUG] Creating Cloudflare Access Application from struct: %+v", newAccessApplication)

accessApplication, err := client.CreateAccessApplication(zoneID, newAccessApplication)
Expand Down Expand Up @@ -86,19 +142,30 @@ func resourceCloudflareAccessApplicationRead(d *schema.ResourceData, meta interf
d.Set("session_duration", accessApplication.SessionDuration)
d.Set("domain", accessApplication.Domain)

corsConfig := convertCORSStructToSchema(d, accessApplication.CorsHeaders)
if corsConfigErr := d.Set("cors_headers", corsConfig); corsConfigErr != nil {
return fmt.Errorf("error setting Access Application CORS header configuration: %s", corsConfigErr)
}

return nil
}

func resourceCloudflareAccessApplicationUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)

updatedAccessApplication := cloudflare.AccessApplication{
ID: d.Id(),
Name: d.Get("name").(string),
Domain: d.Get("domain").(string),
SessionDuration: d.Get("session_duration").(string),
}

if _, ok := d.GetOk("cors_headers"); ok {
CORSConfig, _ := convertCORSSchemaToStruct(d)
updatedAccessApplication.CorsHeaders = CORSConfig
}

log.Printf("[DEBUG] Updating Cloudflare Access Application from struct: %+v", updatedAccessApplication)

accessApplication, err := client.UpdateAccessApplication(zoneID, updatedAccessApplication)
Expand Down Expand Up @@ -148,3 +215,49 @@ func resourceCloudflareAccessApplicationImport(d *schema.ResourceData, meta inte

return []*schema.ResourceData{d}, nil
}

func convertCORSSchemaToStruct(d *schema.ResourceData) (*cloudflare.AccessApplicationCorsHeaders, error) {
CORSConfig := cloudflare.AccessApplicationCorsHeaders{}

if _, ok := d.GetOk("cors_headers"); ok {
if allowedMethods, ok := d.GetOk("cors_headers.0.allowed_methods"); ok {
CORSConfig.AllowedMethods = expandInterfaceToStringList(allowedMethods.(*schema.Set).List())
}

if allowedHeaders, ok := d.GetOk("cors_headers.0.allowed_headers"); ok {
CORSConfig.AllowedHeaders = expandInterfaceToStringList(allowedHeaders.(*schema.Set).List())
}

if allowedOrigins, ok := d.GetOk("cors_headers.0.allowed_origins"); ok {
CORSConfig.AllowedOrigins = expandInterfaceToStringList(allowedOrigins.(*schema.Set).List())
}

CORSConfig.AllowAllMethods = d.Get("cors_headers.0.allow_all_methods").(bool)
CORSConfig.AllowAllHeaders = d.Get("cors_headers.0.allow_all_headers").(bool)
CORSConfig.AllowAllOrigins = d.Get("cors_headers.0.allow_all_origins").(bool)
CORSConfig.AllowCredentials = d.Get("cors_headers.0.allow_credentials").(bool)
CORSConfig.MaxAge = d.Get("cors_headers.0.max_age").(int)
}

return &CORSConfig, nil
}

func convertCORSStructToSchema(d *schema.ResourceData, headers *cloudflare.AccessApplicationCorsHeaders) []interface{} {
if _, ok := d.GetOk("cors_headers"); !ok {
return []interface{}{}
}

m := map[string]interface{}{
"allow_all_methods": headers.AllowAllMethods,
"allow_all_headers": headers.AllowAllHeaders,
"allow_all_origins": headers.AllowAllOrigins,
"allow_credentials": headers.AllowCredentials,
"max_age": headers.MaxAge,
}

m["allowed_methods"] = flattenStringList(headers.AllowedMethods)
m["allowed_headers"] = flattenStringList(headers.AllowedHeaders)
m["allowed_origins"] = flattenStringList(headers.AllowedOrigins)

return []interface{}{m}
}
114 changes: 114 additions & 0 deletions cloudflare/resource_cloudflare_access_application_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cloudflare

import (
"fmt"
"os"
"testing"

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

var (
zoneID = os.Getenv("CLOUDFLARE_ZONE_ID")
domain = os.Getenv("CLOUDFLARE_DOMAIN")
)

func TestAccCloudflareAccessApplicationBasic(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_application.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessApplicationConfigBasic(rnd, zoneID, domain),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
),
},
},
})
}

func TestAccCloudflareAccessApplicationWithCORS(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_application.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessApplicationConfigWithCORS(rnd, zoneID, domain),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "1"),
resource.TestCheckResourceAttr(name, "cors_headers.0.allowed_methods.#", "3"),
resource.TestCheckResourceAttr(name, "cors_headers.0.allowed_origins.#", "1"),
resource.TestCheckResourceAttr(name, "cors_headers.0.max_age", "10"),
),
},
},
})
}

func testAccCloudflareAccessApplicationConfigBasic(rnd, zoneID, domain string) string {
return fmt.Sprintf(`
resource "cloudflare_access_application" "%[1]s" {
zone_id = "%[2]s"
name = "%[1]s"
domain = "%[1]s.%[3]s"
session_duration = "24h"
}
`, rnd, zoneID, domain)
}

func testAccCloudflareAccessApplicationConfigWithCORS(rnd, zoneID, domain string) string {
return fmt.Sprintf(`
resource "cloudflare_access_application" "%[1]s" {
zone_id = "%[2]s"
name = "%[1]s"
domain = "%[1]s.%[3]s"
session_duration = "24h"
cors_headers {
allowed_methods = ["GET", "POST", "OPTIONS"]
allowed_origins = ["https://example.com"]
allow_credentials = true
max_age = 10
}
}
`, rnd, zoneID, domain)
}

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

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

_, err := client.AccessApplication(rs.Primary.Attributes["zone_id"], rs.Primary.ID)
if err == nil {
return fmt.Errorf("AccessApplication still exists")
}
}

return nil
}
35 changes: 34 additions & 1 deletion website/docs/r/access_application.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ resource "cloudflare_access_application" "staging_app" {
domain = "staging.example.com"
session_duration = "24h"
}
# With CORS configuration
resource "cloudflare_access_application" "staging_app" {
zone_id = "1d5fdc9e88c8a8c4518b068cd94331fe"
name = "staging application"
domain = "staging.example.com"
session_duration = "24h"
cors_headers {
allowed_methods = ["GET", "POST", "OPTIONS"]
allowed_origins = ["https://example.com"]
allow_credentials = true
max_age = 10
}
}
```

## Argument Reference
Expand All @@ -33,7 +47,26 @@ The following arguments are supported:
Cloudflare Access in front of. Can include subdomains or paths. Or both.
* `session_duration` - (Optional) How often a user will be forced to
re-authorise. Must be one of `30m`, `6h`, `12h`, `24h`, `168h`, `730h`.

* `cors_headers` - (Optional) CORS configuration for the Access Application. See
below for reference structure.

**cors_headers** allows the following:

* `allowed_methods` - (Optional) List of methods to expose via CORS.
* `allowed_origins` - (Optional) List of origins permitted to make CORS requests.
* `allowed_headers` - (Optional) List of HTTP headers to expose via CORS.
* `allow_all_methods` - (Optional) Boolean value to determine whether all
methods are exposed.
* `allow_all_origins` - (Optional) Boolean value to determine whether all
origins are permitted to make CORS requests.
* `allow_all_headers` - (Optional) Boolean value to determine whether all
HTTP headers are exposed.
* `allow_credentials` - (Optional) Boolean value to determine if credentials
(cookies, authorization headers, or TLS client certificates) are included with
requests.
* `max_age` - (Optional) Integer representing the maximum time a preflight
request will be cached.

## Attributes Reference

The following additional attributes are exported:
Expand Down

0 comments on commit debf20e

Please sign in to comment.