Repository: cloudflare/cloudflare-go
Pull request #1445 (2023-11-20 13:31:26)

Add EmailList to Access Group objects
## Description

Add missing Email List to Access Group objects. While the UI already adds and supports these, the [official API docs](https://developers.cloudflare.com/api/operations/zone-level-access-groups-update-an-access-group?schema_url=https%3A%2F%2Fraw.githubusercontent.com%2Fcloudflare%2Fapi-schemas%2Fmain%2Fopenapi.yaml#request-body) are completely out of date.

## Has your change been tested?

Yes. This change has both unit tests and I also tested it with a short app:
```golang
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/cloudflare/cloudflare-go"
)

func main() {
	api, err := cloudflare.NewWithAPIToken(
		os.Getenv("CF_API_TOKEN"),
		cloudflare.Debug(true),
	)
	accountID := os.Getenv("CF_ACCOUNT_ID")
	rc := cloudflare.AccountIdentifier(accountID)
	ctx := context.Background()

	newAg := cloudflare.CreateAccessGroupParams{
		Name: "test",
		Include: []interface{}{
			cloudflare.AccessGroupEmailDomain{
				EmailDomain: struct {
					Domain string `json:"domain"`
				}{
					Domain: "example.com",
				},
			},
		},
		Require: []interface{}{
			cloudflare.AccessGroupEmailList{
				EmailList: struct {
					ID string `json:"id"`
				}{
					ID: "110c0d2a-cda2-4d25-8451-3cd70c60b8b3",
				},
			},
		},
	}

	_, err = api.CreateAccessGroup(ctx, rc, newAg)
	if err != nil {
		fmt.Println(err)
		return
	}

	ags, _, err := api.ListAccessGroups(ctx, rc, cloudflare.ListAccessGroupsParams{})
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, ag := range ags {
		fmt.Println(ag.ID, ag.Name, ag.Require)
	}
}

```

This outputs (a bit redacted) the following which indicates that it marshals the email_list correctly.
```
POST /client/v4/accounts/xyz/access/groups HTTP/1.1

{"name":"test","include":[{"email_domain":{"domain":"example.com"}}],"exclude":null,"require":[{"email_list":{"id":"110c0d2a-cda2-4d25-8451-3cd70c60b8b3"}}]}
2023/11/20 15:27:55
HTTP/2.0 201 Created

{
  "result": {
    "id": "e1db6929-641f-4996-852a-d9d8a7efa1c1",
    "name": "test",
    "uid": "e1db6929-641f-4996-852a-d9d8a7efa1c1",
    "include": [
      {
        "email_domain": {
          "domain": "example.com"
        }
      }
    ],
    "require": [
      {
        "email_list": {
          "id": "110c0d2a-cda2-4d25-8451-3cd70c60b8b3"
        }
      }
    ],
    "exclude": [],
    "created_at": "2023-11-20T13:27:55Z",
    "updated_at": "2023-11-20T13:27:55Z"
  },
  "success": true,
  "errors": [],
  "messages": []
}
2023/11/20 15:27:55
GET /client/v4/accounts/xyz/access/groups?page=1&per_page=25 HTTP/1.1

2023/11/20 15:27:55
HTTP/2.0 200 OK

{
  "result": [
    {
      "id": "e1db6929-641f-4996-852a-d9d8a7efa1c1",
      "name": "test",
      "uid": "e1db6929-641f-4996-852a-d9d8a7efa1c1",
      "include": [
        {
          "email_domain": {
            "domain": "example.com"
          }
        }
      ],
      "require": [
        {
          "email_list": {
            "id": "110c0d2a-cda2-4d25-8451-3cd70c60b8b3"
          }
        }
      ],
      "exclude": [],
      "created_at": "2023-11-20T13:27:55Z",
      "updated_at": "2023-11-20T13:27:55Z"
    }
  ],
  "success": true,
  "errors": [],
  "messages": []
}
e1db6929-641f-4996-852a-d9d8a7efa1c1 test [map[email_list:map[id:110c0d2a-cda2-4d25-8451-3cd70c60b8b3]]]


```

## 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.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [ ] 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/



Patch:
diff --git a/.changelog/1445.txt b/.changelog/1445.txt
new file mode 100644
index 000000000..664bb2d3e
--- /dev/null
+++ b/.changelog/1445.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+access_group: Add support for email lists
+```
diff --git a/access_group.go b/access_group.go
index bdb3899ad..a521db8e0 100644
--- a/access_group.go
+++ b/access_group.go
@@ -39,6 +39,15 @@ type AccessGroupEmail struct {
 	} `json:"email"`
 }
 
+// AccessGroupEmailList is used for managing access based on the email
+// list. For example, restrict access to users with the email addresses
+// in the email list with the ID `1234567890abcdef1234567890abcdef`.
+type AccessGroupEmailList struct {
+	EmailList struct {
+		ID string `json:"id"`
+	} `json:"email_list"`
+}
+
 // AccessGroupEmailDomain is used for managing access based on an email
 // domain such as `example.com` instead of individual addresses.
 type AccessGroupEmailDomain struct {


Test patch:
diff --git a/access_group_test.go b/access_group_test.go
index 16bbd436e..3acb9acae 100644
--- a/access_group_test.go
+++ b/access_group_test.go
@@ -37,6 +37,16 @@ var (
 			map[string]interface{}{"ip_list": map[string]interface{}{"id": "989d98642c564d2e855e9661899b7252"}},
 		},
 	}
+
+	expectedAccessGroupEmailList = AccessGroup{
+		ID:        "a99d98642c564d2e855e9661899b7252",
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+		Name:      "Allow devs",
+		Include: []interface{}{
+			map[string]interface{}{"email_list": map[string]interface{}{"id": "8a9d98642c564d2e855e9661899b7252"}},
+		},
+	}
 )
 
 func TestAccessGroups(t *testing.T) {
@@ -406,3 +416,56 @@ func TestCreateIPListAccessGroup(t *testing.T) {
 		assert.Equal(t, expectedAccessGroupIpList, actual)
 	}
 }
+
+func TestCreateEmailListAccessGroup(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "a99d98642c564d2e855e9661899b7252",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"name": "Allow devs",
+				"include": [
+					{
+						"email_list": {
+							"id": "8a9d98642c564d2e855e9661899b7252"
+						}
+					}
+				]
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/groups", handler)
+
+	accessGroup := CreateAccessGroupParams{
+		Name: "Allow devs by email_list",
+		Include: []interface{}{
+			AccessGroupEmailList{struct {
+				ID string `json:"id"`
+			}{ID: "989d98642c564d2e855e9661899b7252"}},
+		},
+	}
+
+	actual, err := client.CreateAccessGroup(context.Background(), testAccountRC, accessGroup)
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedAccessGroupEmailList, actual)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/access/groups", handler)
+
+	actual, err = client.CreateAccessGroup(context.Background(), testZoneRC, accessGroup)
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedAccessGroupEmailList, actual)
+	}
+}