Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AUTH-5403 added access custom pages as a new resource #2643

Merged
merged 6 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/2643.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:new-resource
cloudflare_access_custom_page
```

```release-note:enhancement
resource/cloudflare_access_application: adds the ability to associate a custom page with an application.
```

```release-note:enhancement
resource/cloudflare_access_organization: adds the ability to associate a custom page with an organization.
```
1 change: 1 addition & 0 deletions docs/resources/access_application.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ resource "cloudflare_access_application" "staging_app" {
- `cors_headers` (Block List) CORS configuration for the Access Application. See below for reference structure. (see [below for nested schema](#nestedblock--cors_headers))
- `custom_deny_message` (String) Option that returns a custom error message when a user is denied access to the application.
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application.
- `custom_pages` (Set of String) The custom pages selected for the application.
- `domain` (String) The primary hostname and path that Access will secure. If the app is visible in the App Launcher dashboard, this is the domain that will be displayed.
- `enable_binding_cookie` (Boolean) Option to provide increased security against compromised authorization tokens and CSRF attacks by requiring an additional "binding" cookie on requests. Defaults to `false`.
- `http_only_cookie_attribute` (Boolean) Option to add the `HttpOnly` cookie flag to access tokens.
Expand Down
43 changes: 43 additions & 0 deletions docs/resources/access_custom_page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
page_title: "cloudflare_access_custom_page Resource - Cloudflare"
subcategory: ""
description: |-
Provides a resource to customize the pages your end users will see
when trying to reach applications behind Cloudflare Access.
---

# cloudflare_access_custom_page (Resource)

Provides a resource to customize the pages your end users will see
when trying to reach applications behind Cloudflare Access.

## Example Usage

```terraform
resource "cloudflare_access_custom_page" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
name = "example"
type = "forbidden"
custom_html = "<html><body><h1>Forbidden</h1></body></html>"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Friendly name of the Access Custom Page configuration.
- `type` (String) Type of Access custom page to create. Available values: `identity_denied`, `forbidden`.

### Optional

- `account_id` (String) The account identifier to target for the resource. Conflicts with `zone_id`. **Modifying this attribute will force creation of a new resource.**
- `app_count` (Number) Number of apps to display on the custom page.
- `custom_html` (String) Custom HTML to display on the custom page.
- `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`. **Modifying this attribute will force creation of a new resource.**

### Read-Only

- `id` (String) The ID of this resource.


10 changes: 10 additions & 0 deletions docs/resources/access_organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ resource "cloudflare_access_organization" "example" {

- `account_id` (String) The account identifier to target for the resource. Conflicts with `zone_id`.
- `auto_redirect_to_identity` (Boolean) When set to true, users skip the identity provider selection step during login.
- `custom_pages` (Block List) Custom pages for your Zero Trust organization. (see [below for nested schema](#nestedblock--custom_pages))
- `is_ui_read_only` (Boolean) When set to true, this will disable all editing of Access resources via the Zero Trust Dashboard.
- `login_design` (Block List) (see [below for nested schema](#nestedblock--login_design))
- `name` (String) The name of your Zero Trust organization.
Expand All @@ -51,6 +52,15 @@ resource "cloudflare_access_organization" "example" {

- `id` (String) The ID of this resource.

<a id="nestedblock--custom_pages"></a>
### Nested Schema for `custom_pages`

Optional:

- `forbidden` (String) The id of the forbidden page.
- `identity_denied` (String) The id of the identity denied page.


<a id="nestedblock--login_design"></a>
### Nested Schema for `login_design`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "cloudflare_access_custom_page" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
name = "example"
type = "forbidden"
custom_html = "<html><body><h1>Forbidden</h1></body></html>"
}
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_access_ca_certificate": resourceCloudflareAccessCACertificate(),
"cloudflare_access_group": resourceCloudflareAccessGroup(),
"cloudflare_access_identity_provider": resourceCloudflareAccessIdentityProvider(),
"cloudflare_access_custom_page": resourceCloudflareAccessCustomPage(),
"cloudflare_access_keys_configuration": resourceCloudflareAccessKeysConfiguration(),
"cloudflare_access_mutual_tls_certificate": resourceCloudflareAccessMutualTLSCertificate(),
"cloudflare_access_organization": resourceCloudflareAccessOrganization(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
newAccessApplication.AllowedIdps = expandInterfaceToStringList(value.(*schema.Set).List())
}

if value, ok := d.GetOk("custom_pages"); ok {
newAccessApplication.CustomPages = expandInterfaceToStringList(value.(*schema.Set).List())
}

if value, ok := d.GetOk("self_hosted_domains"); ok {
newAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
}
Expand Down Expand Up @@ -127,6 +131,7 @@ func resourceCloudflareAccessApplicationRead(ctx context.Context, d *schema.Reso
d.Set("logo_url", accessApplication.LogoURL)
d.Set("app_launcher_visible", accessApplication.AppLauncherVisible)
d.Set("service_auth_401_redirect", accessApplication.ServiceAuth401Redirect)
d.Set("custom_pages", accessApplication.CustomPages)

corsConfig := convertCORSStructToSchema(d, accessApplication.CorsHeaders)
if corsConfigErr := d.Set("cors_headers", corsConfig); corsConfigErr != nil {
Expand Down Expand Up @@ -176,6 +181,10 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
updatedAccessApplication.AllowedIdps = expandInterfaceToStringList(value.(*schema.Set).List())
}

if value, ok := d.GetOk("custom_pages"); ok {
updatedAccessApplication.CustomPages = expandInterfaceToStringList(value.(*schema.Set).List())
}

if value, ok := d.GetOk("self_hosted_domains"); ok {
updatedAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
}
Expand Down
130 changes: 130 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_access_custom_page.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package sdkv2provider

import (
"context"
"errors"
"fmt"

"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceCloudflareAccessCustomPage() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareAccessCustomPageSchema(),
CreateContext: resourceCloudflareAccessCustomPageCreate,
ReadContext: resourceCloudflareAccessCustomPageRead,
UpdateContext: resourceCloudflareAccessCustomPageUpdate,
DeleteContext: resourceCloudflareAccessCustomPageDelete,
Description: heredoc.Doc(`
Provides a resource to customize the pages your end users will see
when trying to reach applications behind Cloudflare Access.
`),
}
}

func resourceCloudflareAccessCustomPageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

accessCustomPage, err := client.GetAccessCustomPage(ctx, identifier, d.Id())
if err != nil {
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("Access Custom Page %s no longer exists", d.Id()))
d.SetId("")
return nil
}
return diag.FromErr(fmt.Errorf("error fetching Access Custom Page: %w", err))
}

d.SetId(accessCustomPage.UID)
d.Set("name", accessCustomPage.Name)
d.Set("type", accessCustomPage.Type)
d.Set("custom_html", accessCustomPage.CustomHTML)
d.Set("app_count", accessCustomPage.AppCount)

return nil
}

func resourceCloudflareAccessCustomPageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

cpType := d.Get("type").(string)
customPage := cloudflare.CreateAccessCustomPageParams{
Name: d.Get("name").(string),
Type: cloudflare.AccessCustomPageType(cpType),
CustomHTML: d.Get("custom_html").(string),
}

tflog.Debug(ctx, fmt.Sprintf("Creating Access Custom Page: %+v", customPage))

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

accessCustomPage, err := client.CreateAccessCustomPage(ctx, identifier, customPage)
if err != nil {
return diag.FromErr(fmt.Errorf("error creating Access Custom Page: %w", err))
}

d.SetId(accessCustomPage.UID)

return resourceCloudflareAccessCustomPageRead(ctx, d, meta)
}

func resourceCloudflareAccessCustomPageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

cpType := d.Get("type").(string)
customPage := cloudflare.UpdateAccessCustomPageParams{
Name: d.Get("name").(string),
Type: cloudflare.AccessCustomPageType(cpType),
CustomHTML: d.Get("custom_html").(string),
UID: d.Id(),
}

tflog.Debug(ctx, fmt.Sprintf("Updating Access Custom Page: %+v", customPage))

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

accessCustomPage, err := client.UpdateAccessCustomPage(ctx, identifier, customPage)
if err != nil {
return diag.FromErr(fmt.Errorf("error updating Access Custom Page: %w", err))
}

if accessCustomPage.UID == "" {
return diag.FromErr(fmt.Errorf("failed to find Access Custom Page ID in update response; resource was empty"))
}

return resourceCloudflareAccessCustomPageRead(ctx, d, meta)
}

func resourceCloudflareAccessCustomPageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

err = client.DeleteAccessCustomPage(ctx, identifier, d.Id())
if err != nil {
return diag.FromErr(fmt.Errorf("error deleting Access Custom Page: %w", err))
}

d.SetId("")

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package sdkv2provider

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccCloudflareAccessCustomPage_IdentityDenied(t *testing.T) {
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
t.Setenv("CLOUDFLARE_API_TOKEN", "")
}

rnd := generateRandomResourceName()
resourceName := fmt.Sprintf("cloudflare_access_custom_page.%s", rnd)
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareAccessCustomPage_CustomHTML(rnd, zoneID, "identity_denied", "<html><body><h1>Access Denied</h1></body></html>"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "type", "identity_denied"),
resource.TestCheckResourceAttr(resourceName, "custom_html", "<html><body><h1>Access Denied</h1></body></html>"),
),
},
},
})
}

func TestAccCloudflareAccessCustomPage_Forbidden(t *testing.T) {
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
t.Setenv("CLOUDFLARE_API_TOKEN", "")
}

rnd := generateRandomResourceName()
resourceName := fmt.Sprintf("cloudflare_access_custom_page.%s", rnd)
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareAccessCustomPage_CustomHTML(rnd, zoneID, "forbidden", "<html><body><h1>Forbidden</h1></body></html>"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", rnd),
resource.TestCheckResourceAttr(resourceName, "type", "forbidden"),
resource.TestCheckResourceAttr(resourceName, "custom_html", "<html><body><h1>Forbidden</h1></body></html>"),
),
},
},
})
}

func testAccCheckCloudflareAccessCustomPage_CustomHTML(rnd, zoneID, pageType, markup string) string {
return fmt.Sprintf(`
resource "cloudflare_access_custom_page" "%[1]s" {
zone_id = "%[2]s"
name = "%[1]s"
type = "%[3]s"
custom_html = "%[4]s"
}
`, rnd, zoneID, pageType, markup)
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ func resourceCloudflareAccessOrganizationRead(ctx context.Context, d *schema.Res
return diag.FromErr(fmt.Errorf("error setting Access Organization Login Design configuration: %w", loginDesignErr))
}

if &organization.CustomPages != nil {
customPages := convertCustomPageStructToSchema(ctx, d, &organization.CustomPages)
if customPagesErr := d.Set("custom_pages", customPages); customPagesErr != nil {
return diag.FromErr(fmt.Errorf("error setting Access Organization Custom Pages configuration: %w", customPagesErr))
}
}

return nil
}

Expand All @@ -82,6 +89,11 @@ func resourceCloudflareAccessOrganizationUpdate(ctx context.Context, d *schema.R
loginDesign := convertLoginDesignSchemaToStruct(d)
updatedAccessOrganization.LoginDesign = *loginDesign

if _, ok := d.GetOk("custom_pages"); ok {
customPagesStruct := convertCustomPageSchemaToStruct(d)
updatedAccessOrganization.CustomPages = *customPagesStruct
}

tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Access Organization from struct: %+v", updatedAccessOrganization))

identifier, err := initIdentifier(d)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Default: false,
Description: "Option to return a 401 status code in service authentication rules on failed requests.",
},
"custom_pages": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "The custom pages selected for the application.",
},
}
}

Expand Down
Loading