Repository: cloudflare/cloudflare-go
Pull request #1343 (2023-07-31 17:00:16)

AUTH-5403 added access custom pages
<!-- 
Provide a general summary of your changes in the title above. You should
remove this overview, any sections and any section descriptions you
don't need below before submitting. There isn't a strict requirement to
use this template if you can structure your description and still cover
these points. 
-->

## Description

This PR will add the ability to CRUD Access custom pages

<!--
Describe your changes in detail through motivation and context. Why is
this change required? What problem does it solve? If it fixes an open
issue, link to the issue using GitHub's closing issues keywords[1].
-->
## Has your change been tested?

Via units tests

<!--
Explain how the change has been tested and what you ran to confirm your
change affects other parts of the code. Automated tests are generally
expected and changes without tests should explain why they aren't
required.
-->

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [X] My change requires a change to the documentation.
- [X] I have updated the documentation accordingly.
- [X] I have added tests to cover my changes.
- [X] All new and existing tests passed.
- [X] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.

Patch:
diff --git a/.changelog/1343.txt b/.changelog/1343.txt
new file mode 100644
index 000000000..df6721b23
--- /dev/null
+++ b/.changelog/1343.txt
@@ -0,0 +1,11 @@
+```release-note:enhancement
+access_application: Add support for custom pages
+```
+
+```release-note:enhancement
+access_custom_page: Add support for custom pages
+```
+
+```release-note:enhancement
+access_organization: add support for custom pages
+```
diff --git a/access_application.go b/access_application.go
index 1c8bd66e6..c43faf0c8 100644
--- a/access_application.go
+++ b/access_application.go
@@ -50,6 +50,7 @@ type AccessApplication struct {
 	HttpOnlyCookieAttribute *bool                          `json:"http_only_cookie_attribute,omitempty"`
 	ServiceAuth401Redirect  *bool                          `json:"service_auth_401_redirect,omitempty"`
 	PathCookieAttribute     *bool                          `json:"path_cookie_attribute,omitempty"`
+	CustomPages             []string                       `json:"custom_pages,omitempty"`
 }
 
 type AccessApplicationGatewayRule struct {
@@ -139,6 +140,7 @@ type CreateAccessApplicationParams struct {
 	SessionDuration         string                         `json:"session_duration,omitempty"`
 	SkipInterstitial        *bool                          `json:"skip_interstitial,omitempty"`
 	Type                    AccessApplicationType          `json:"type,omitempty"`
+	CustomPages             []string                       `json:"custom_pages,omitempty"`
 }
 
 type UpdateAccessApplicationParams struct {
@@ -165,6 +167,7 @@ type UpdateAccessApplicationParams struct {
 	SessionDuration         string                         `json:"session_duration,omitempty"`
 	SkipInterstitial        *bool                          `json:"skip_interstitial,omitempty"`
 	Type                    AccessApplicationType          `json:"type,omitempty"`
+	CustomPages             []string                       `json:"custom_pages,omitempty"`
 }
 
 // ListAccessApplications returns all applications within an account or zone.
diff --git a/access_custom_page.go b/access_custom_page.go
new file mode 100644
index 000000000..e521fbcde
--- /dev/null
+++ b/access_custom_page.go
@@ -0,0 +1,126 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+var ErrMissingUID = errors.New("required UID missing")
+
+type AccessCustomPageType string
+
+const (
+	Forbidden      AccessCustomPageType = "forbidden"
+	IdentityDenied AccessCustomPageType = "identity_denied"
+)
+
+type AccessCustomPage struct {
+	// The HTML content of the custom page.
+	CustomHTML string               `json:"custom_html,omitempty"`
+	Name       string               `json:"name,omitempty"`
+	AppCount   int                  `json:"app_count,omitempty"`
+	Type       AccessCustomPageType `json:"type,omitempty"`
+	UID        string               `json:"uid,omitempty"`
+}
+
+type AccessCustomPageListResponse struct {
+	Response
+	Result     []AccessCustomPage `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+type AccessCustomPageResponse struct {
+	Response
+	Result AccessCustomPage `json:"result"`
+}
+
+type ListAccessCustomPagesParams struct{}
+
+type CreateAccessCustomPageParams struct {
+	CustomHTML string               `json:"custom_html,omitempty"`
+	Name       string               `json:"name,omitempty"`
+	Type       AccessCustomPageType `json:"type,omitempty"`
+}
+
+type UpdateAccessCustomPageParams struct {
+	CustomHTML string               `json:"custom_html,omitempty"`
+	Name       string               `json:"name,omitempty"`
+	Type       AccessCustomPageType `json:"type,omitempty"`
+	UID        string               `json:"uid,omitempty"`
+}
+
+func (api *API) ListAccessCustomPages(ctx context.Context, rc *ResourceContainer, params ListAccessCustomPagesParams) ([]AccessCustomPage, error) {
+	uri := buildURI(fmt.Sprintf("/%s/%s/access/custom_pages", rc.Level, rc.Identifier), params)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []AccessCustomPage{}, err
+	}
+
+	var customPagesResponse AccessCustomPageListResponse
+	err = json.Unmarshal(res, &customPagesResponse)
+	if err != nil {
+		return []AccessCustomPage{}, err
+	}
+	return customPagesResponse.Result, nil
+}
+
+func (api *API) GetAccessCustomPage(ctx context.Context, rc *ResourceContainer, id string) (AccessCustomPage, error) {
+	uri := fmt.Sprintf("/%s/%s/access/custom_pages/%s", rc.Level, rc.Identifier, id)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+
+	var customPageResponse AccessCustomPageResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+	return customPageResponse.Result, nil
+}
+
+func (api *API) CreateAccessCustomPage(ctx context.Context, rc *ResourceContainer, params CreateAccessCustomPageParams) (AccessCustomPage, error) {
+	uri := fmt.Sprintf("/%s/%s/access/custom_pages", rc.Level, rc.Identifier)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+
+	var customPageResponse AccessCustomPageResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+	return customPageResponse.Result, nil
+}
+
+func (api *API) DeleteAccessCustomPage(ctx context.Context, rc *ResourceContainer, id string) error {
+	uri := fmt.Sprintf("/%s/%s/access/custom_pages/%s", rc.Level, rc.Identifier, id)
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (api *API) UpdateAccessCustomPage(ctx context.Context, rc *ResourceContainer, params UpdateAccessCustomPageParams) (AccessCustomPage, error) {
+	if params.UID == "" {
+		return AccessCustomPage{}, ErrMissingUID
+	}
+
+	uri := fmt.Sprintf("/%s/%s/access/custom_pages/%s", rc.Level, rc.Identifier, params.UID)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+
+	var customPageResponse AccessCustomPageResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return AccessCustomPage{}, err
+	}
+	return customPageResponse.Result, nil
+}
diff --git a/access_organization.go b/access_organization.go
index d7f616cf5..e0242b99d 100644
--- a/access_organization.go
+++ b/access_organization.go
@@ -19,6 +19,7 @@ type AccessOrganization struct {
 	UIReadOnlyToggleReason         string                        `json:"ui_read_only_toggle_reason,omitempty"`
 	UserSeatExpirationInactiveTime string                        `json:"user_seat_expiration_inactive_time,omitempty"`
 	AutoRedirectToIdentity         *bool                         `json:"auto_redirect_to_identity,omitempty"`
+	CustomPages                    AccessOrganizationCustomPages `json:"custom_pages,omitempty"`
 }
 
 // AccessOrganizationLoginDesign represents the login design options.
@@ -30,6 +31,11 @@ type AccessOrganizationLoginDesign struct {
 	FooterText      string `json:"footer_text"`
 }
 
+type AccessOrganizationCustomPages struct {
+	Forbidden      AccessCustomPageType `json:"forbidden,omitempty"`
+	IdentityDenied AccessCustomPageType `json:"identity_denied,omitempty"`
+}
+
 // AccessOrganizationListResponse represents the response from the list
 // access organization endpoint.
 type AccessOrganizationListResponse struct {
@@ -57,6 +63,7 @@ type CreateAccessOrganizationParams struct {
 	UIReadOnlyToggleReason         string                        `json:"ui_read_only_toggle_reason,omitempty"`
 	UserSeatExpirationInactiveTime string                        `json:"user_seat_expiration_inactive_time,omitempty"`
 	AutoRedirectToIdentity         *bool                         `json:"auto_redirect_to_identity,omitempty"`
+	CustomPages                    AccessOrganizationCustomPages `json:"custom_pages,omitempty"`
 }
 
 type UpdateAccessOrganizationParams struct {
@@ -67,6 +74,7 @@ type UpdateAccessOrganizationParams struct {
 	UIReadOnlyToggleReason         string                        `json:"ui_read_only_toggle_reason,omitempty"`
 	UserSeatExpirationInactiveTime string                        `json:"user_seat_expiration_inactive_time,omitempty"`
 	AutoRedirectToIdentity         *bool                         `json:"auto_redirect_to_identity,omitempty"`
+	CustomPages                    AccessOrganizationCustomPages `json:"custom_pages,omitempty"`
 }
 
 func (api *API) GetAccessOrganization(ctx context.Context, rc *ResourceContainer, params GetAccessOrganizationParams) (AccessOrganization, ResultInfo, error) {


Test patch:
diff --git a/access_application_test.go b/access_application_test.go
index 3c07a5443..0309b6c74 100644
--- a/access_application_test.go
+++ b/access_application_test.go
@@ -46,7 +46,8 @@ func TestAccessApplications(t *testing.T) {
 					"skip_interstitial": true,
 					"app_launcher_visible": true,
 					"service_auth_401_redirect": true,
-					"path_cookie_attribute": true
+					"path_cookie_attribute": true,
+					"custom_pages": ["480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"]
 				}
 			],
 			"result_info": {
@@ -83,6 +84,7 @@ func TestAccessApplications(t *testing.T) {
 		LogoURL:                 "https://www.example.com/example.png",
 		SkipInterstitial:        BoolPtr(true),
 		PathCookieAttribute:     BoolPtr(true),
+		CustomPages:             []string{"480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"},
 	}}
 
 	mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", handler)
diff --git a/access_custom_page_test.go b/access_custom_page_test.go
new file mode 100644
index 000000000..c81f034cd
--- /dev/null
+++ b/access_custom_page_test.go
@@ -0,0 +1,196 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAccessCustomPages(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodGet, "HTTP method")
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"name": "Forbidden",
+					"app_count": 0,
+					"type": "forbidden",
+					"uid": "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"
+				}
+			],
+			"result_info": {
+				"page": 1,
+				"per_page": 20,
+				"count": 1,
+				"total_count": 1
+			}
+		}`)
+	}
+
+	want := []AccessCustomPage{
+		{
+			Name:     "Forbidden",
+			AppCount: 0,
+			Type:     Forbidden,
+			UID:      "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc",
+		},
+	}
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/custom_pages", handler)
+	actual, err := client.ListAccessCustomPages(context.Background(), AccountIdentifier(testAccountID), ListAccessCustomPagesParams{})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestAccessCustomPage(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodGet, "HTTP method")
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"custom_html": "<html><body><h1>Forbidden</h1></body></html>",
+				"name": "Forbidden",
+				"app_count": 0,
+				"type": "forbidden",
+				"uid": "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"
+			}
+		}`)
+	}
+
+	want := AccessCustomPage{
+		Name:       "Forbidden",
+		AppCount:   0,
+		Type:       Forbidden,
+		UID:        "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc",
+		CustomHTML: "<html><body><h1>Forbidden</h1></body></html>",
+	}
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/custom_pages/480f4f69-1a28-4fdd-9240-1ed29f0ac1dc", handler)
+	actual, err := client.GetAccessCustomPage(context.Background(), AccountIdentifier(testAccountID), "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestCreateAccessCustomPage(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodPost, "HTTP method")
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"custom_html": "<html><body><h1>Forbidden</h1></body></html>",
+				"name": "Forbidden",
+				"app_count": 0,
+				"type": "forbidden",
+				"uid": "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"
+			}
+		}`)
+	}
+
+	customPage := AccessCustomPage{
+		Name:       "Forbidden",
+		AppCount:   0,
+		Type:       Forbidden,
+		UID:        "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc",
+		CustomHTML: "<html><body><h1>Forbidden</h1></body></html>",
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/custom_pages", handler)
+	actual, err := client.CreateAccessCustomPage(context.Background(), AccountIdentifier(testAccountID), CreateAccessCustomPageParams{
+		Name:       "Forbidden",
+		Type:       Forbidden,
+		CustomHTML: "<html><body><h1>Forbidden</h1></body></html>",
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, customPage, actual)
+	}
+}
+
+func TestUpdateAccessCustomPage(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodPut, "HTTP method")
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"custom_html": "<html><body><h1>Forbidden</h1></body></html>",
+				"name": "Forbidden",
+				"app_count": 0,
+				"type": "forbidden",
+				"uid": "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"
+			}
+		}`)
+	}
+
+	customPage := AccessCustomPage{
+		Name:       "Forbidden",
+		AppCount:   0,
+		Type:       Forbidden,
+		UID:        "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc",
+		CustomHTML: "<html><body><h1>Forbidden</h1></body></html>",
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/custom_pages/480f4f69-1a28-4fdd-9240-1ed29f0ac1dc", handler)
+	actual, err := client.UpdateAccessCustomPage(context.Background(), AccountIdentifier(testAccountID), UpdateAccessCustomPageParams{
+		UID:        "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc",
+		Name:       "Forbidden",
+		Type:       Forbidden,
+		CustomHTML: "<html><body><h1>Forbidden</h1></body></html>",
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, customPage, actual)
+	}
+}
+
+func TestDeleteAccessCustomPage(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodDelete, "HTTP method")
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			result: {
+				"id": "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/custom_pages/480f4f69-1a28-4fdd-9240-1ed29f0ac1dc", handler)
+	err := client.DeleteAccessCustomPage(context.Background(), AccountIdentifier(testAccountID), "480f4f69-1a28-4fdd-9240-1ed29f0ac1dc")
+
+	assert.NoError(t, err)
+}