Repository: cloudflare/cloudflare-go
Pull request #339 (2019-08-20 06:30:53)

Add support for Access Service Tokens
Updates the library to support creating, updating and deleting a Service
Token for Cloudflare Access.

API documentation: https://api.cloudflare.com/#access-service-tokens


Patch:
diff --git a/access_organization.go b/access_organization.go
new file mode 100644
index 000000000..5bc4a16aa
--- /dev/null
+++ b/access_organization.go
@@ -0,0 +1,101 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessOrganization represents an Access organization.
+type AccessOrganization struct {
+	CreatedAt   *time.Time                    `json:"created_at"`
+	UpdatedAt   *time.Time                    `json:"updated_at"`
+	Name        string                        `json:"name"`
+	AuthDomain  string                        `json:"auth_domain"`
+	LoginDesign AccessOrganizationLoginDesign `json:"login_design"`
+}
+
+// AccessOrganizationLoginDesign represents the login design options.
+type AccessOrganizationLoginDesign struct {
+	BackgroundColor string `json:"background_color"`
+	TextColor       string `json:"text_color"`
+	LogoPath        string `json:"logo_path"`
+}
+
+// AccessOrganizationListResponse represents the response from the list
+// access organization endpoint.
+type AccessOrganizationListResponse struct {
+	Result AccessOrganization `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessOrganizationDetailResponse is the API response, containing a
+// single access organization.
+type AccessOrganizationDetailResponse struct {
+	Success  bool               `json:"success"`
+	Errors   []string           `json:"errors"`
+	Messages []string           `json:"messages"`
+	Result   AccessOrganization `json:"result"`
+}
+
+// AccessOrganization returns the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-access-organization-details
+func (api *API) AccessOrganization(accountID string) (AccessOrganization, ResultInfo, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccessOrganization{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationListResponse AccessOrganizationListResponse
+	err = json.Unmarshal(res, &accessOrganizationListResponse)
+	if err != nil {
+		return AccessOrganization{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationListResponse.Result, accessOrganizationListResponse.ResultInfo, nil
+}
+
+// CreateAccessOrganization creates the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-create-access-organization
+func (api *API) CreateAccessOrganization(accountID string, accessOrganization AccessOrganization) (AccessOrganization, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("POST", uri, accessOrganization)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationDetailResponse AccessOrganizationDetailResponse
+	err = json.Unmarshal(res, &accessOrganizationDetailResponse)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationDetailResponse.Result, nil
+}
+
+// UpdateAccessOrganization creates the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-update-access-organization
+func (api *API) UpdateAccessOrganization(accountID string, accessOrganization AccessOrganization) (AccessOrganization, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("PUT", uri, accessOrganization)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationDetailResponse AccessOrganizationDetailResponse
+	err = json.Unmarshal(res, &accessOrganizationDetailResponse)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationDetailResponse.Result, nil
+}
diff --git a/access_service_tokens.go b/access_service_tokens.go
new file mode 100644
index 000000000..66a2bb794
--- /dev/null
+++ b/access_service_tokens.go
@@ -0,0 +1,167 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessServiceToken represents an Access Service Token.
+type AccessServiceToken struct {
+	ClientID  string     `json:"client_id"`
+	CreatedAt *time.Time `json:"created_at"`
+	ExpiresAt *time.Time `json:"expires_at"`
+	ID        string     `json:"id"`
+	Name      string     `json:"name"`
+	UpdatedAt *time.Time `json:"updated_at"`
+}
+
+// AccessServiceTokenUpdateResponse represents the response from the API
+// when a new Service Token is updated. This base struct is also used in the
+// Create as they are very similar responses.
+type AccessServiceTokenUpdateResponse struct {
+	CreatedAt *time.Time `json:"created_at"`
+	UpdatedAt *time.Time `json:"updated_at"`
+	ID        string     `json:"id"`
+	Name      string     `json:"name"`
+	ClientID  string     `json:"client_id"`
+}
+
+// AccessServiceTokenCreateResponse is the same API response as the Update
+// operation with the exception that the `ClientSecret` is present in a
+// Create operation.
+type AccessServiceTokenCreateResponse struct {
+	CreatedAt    *time.Time `json:"created_at"`
+	UpdatedAt    *time.Time `json:"updated_at"`
+	ID           string     `json:"id"`
+	Name         string     `json:"name"`
+	ClientID     string     `json:"client_id"`
+	ClientSecret string     `json:"client_secret"`
+}
+
+// AccessServiceTokensListResponse represents the response from the list
+// Access Service Tokens endpoint.
+type AccessServiceTokensListResponse struct {
+	Result []AccessServiceToken `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessServiceTokensDetailResponse is the API response, containing a single
+// Access Service Token.
+type AccessServiceTokensDetailResponse struct {
+	Success  bool               `json:"success"`
+	Errors   []string           `json:"errors"`
+	Messages []string           `json:"messages"`
+	Result   AccessServiceToken `json:"result"`
+}
+
+// AccessServiceTokensCreationDetailResponse is the API response, containing a
+// single Access Service Token.
+type AccessServiceTokensCreationDetailResponse struct {
+	Success  bool                             `json:"success"`
+	Errors   []string                         `json:"errors"`
+	Messages []string                         `json:"messages"`
+	Result   AccessServiceTokenCreateResponse `json:"result"`
+}
+
+// AccessServiceTokensUpdateDetailResponse is the API response, containing a
+// single Access Service Token.
+type AccessServiceTokensUpdateDetailResponse struct {
+	Success  bool                             `json:"success"`
+	Errors   []string                         `json:"errors"`
+	Messages []string                         `json:"messages"`
+	Result   AccessServiceTokenUpdateResponse `json:"result"`
+}
+
+// AccessServiceTokens returns all Access Service Tokens for an account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-list-access-service-tokens
+func (api *API) AccessServiceTokens(accountID string) ([]AccessServiceToken, ResultInfo, error) {
+	uri := "/accounts/" + accountID + "/access/service_tokens"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccessServiceToken{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokensListResponse AccessServiceTokensListResponse
+	err = json.Unmarshal(res, &accessServiceTokensListResponse)
+	if err != nil {
+		return []AccessServiceToken{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokensListResponse.Result, accessServiceTokensListResponse.ResultInfo, nil
+}
+
+// CreateAccessServiceToken creates a new Access Service Token for an account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-create-access-service-token
+func (api *API) CreateAccessServiceToken(accountID, name string) (AccessServiceTokenCreateResponse, error) {
+	uri := "/accounts/" + accountID + "/access/service_tokens"
+	marshalledName, _ := json.Marshal(struct {
+		Name string `json:"name"`
+	}{name})
+
+	res, err := api.makeRequest("POST", uri, marshalledName)
+
+	if err != nil {
+		return AccessServiceTokenCreateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenCreation AccessServiceTokensCreationDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenCreation)
+	if err != nil {
+		return AccessServiceTokenCreateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenCreation.Result, nil
+}
+
+// UpdateAccessServiceToken updates an existing Access Service Token for an
+// account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-update-access-service-token
+func (api *API) UpdateAccessServiceToken(accountID, uuid, name string) (AccessServiceTokenUpdateResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/access/service_tokens/%s", accountID, uuid)
+
+	marshalledName, _ := json.Marshal(struct {
+		Name string `json:"name"`
+	}{name})
+
+	res, err := api.makeRequest("PUT", uri, marshalledName)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenUpdate AccessServiceTokensUpdateDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenUpdate)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenUpdate.Result, nil
+}
+
+// DeleteAccessServiceToken removes an existing Access Service Token for an
+// account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-delete-access-service-token
+func (api *API) DeleteAccessServiceToken(accountID, uuid string) (AccessServiceTokenUpdateResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/access/service_tokens/%s", accountID, uuid)
+
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenUpdate AccessServiceTokensUpdateDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenUpdate)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenUpdate.Result, nil
+}


Test patch:
diff --git a/access_organization_test.go b/access_organization_test.go
new file mode 100644
index 000000000..cc6c11a72
--- /dev/null
+++ b/access_organization_test.go
@@ -0,0 +1,157 @@
+package cloudflare
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAccessOrganization(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"name": "Widget Corps Internal Applications",
+				"auth_domain": "test.cloudflareaccess.com",
+				"login_design": {
+					"background_color": "#c5ed1b",
+					"text_color": "#c5ed1b",
+					"logo_path": "https://example.com/logo.png"
+				}
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/organizations", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := AccessOrganization{
+		Name:       "Widget Corps Internal Applications",
+		CreatedAt:  &createdAt,
+		UpdatedAt:  &updatedAt,
+		AuthDomain: "test.cloudflareaccess.com",
+		LoginDesign: AccessOrganizationLoginDesign{
+			BackgroundColor: "#c5ed1b",
+			TextColor:       "#c5ed1b",
+			LogoPath:        "https://example.com/logo.png",
+		},
+	}
+
+	actual, _, err := client.AccessOrganization("01a7362d577a6c3019a474fd6f485823")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestCreateAccessOrganization(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"name": "Widget Corps Internal Applications",
+				"auth_domain": "test.cloudflareaccess.com",
+				"login_design": {
+					"background_color": "#c5ed1b",
+					"text_color": "#c5ed1b",
+					"logo_path": "https://example.com/logo.png"
+				}
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/organizations", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := AccessOrganization{
+		Name:       "Widget Corps Internal Applications",
+		CreatedAt:  &createdAt,
+		UpdatedAt:  &updatedAt,
+		AuthDomain: "test.cloudflareaccess.com",
+		LoginDesign: AccessOrganizationLoginDesign{
+			BackgroundColor: "#c5ed1b",
+			TextColor:       "#c5ed1b",
+			LogoPath:        "https://example.com/logo.png",
+		},
+	}
+
+	actual, err := client.CreateAccessOrganization("01a7362d577a6c3019a474fd6f485823", want)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateAccessOrganization(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "PUT", "Expected method 'PUT', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"name": "Widget Corps Internal Applications",
+				"auth_domain": "test.cloudflareaccess.com",
+				"login_design": {
+					"background_color": "#c5ed1b",
+					"text_color": "#c5ed1b",
+					"logo_path": "https://example.com/logo.png"
+				}
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/organizations", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := AccessOrganization{
+		Name:       "Widget Corps Internal Applications",
+		CreatedAt:  &createdAt,
+		UpdatedAt:  &updatedAt,
+		AuthDomain: "test.cloudflareaccess.com",
+		LoginDesign: AccessOrganizationLoginDesign{
+			BackgroundColor: "#c5ed1b",
+			TextColor:       "#c5ed1b",
+			LogoPath:        "https://example.com/logo.png",
+		},
+	}
+
+	actual, err := client.UpdateAccessOrganization("01a7362d577a6c3019a474fd6f485823", want)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
diff --git a/access_service_tokens_test.go b/access_service_tokens_test.go
new file mode 100644
index 000000000..a6097013c
--- /dev/null
+++ b/access_service_tokens_test.go
@@ -0,0 +1,179 @@
+package cloudflare
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAccessServiceTokens(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"created_at": "2014-01-01T05:20:00.12345Z",
+					"updated_at": "2014-01-01T05:20:00.12345Z",
+					"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+					"name": "CI/CD token",
+					"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com"
+				}
+			]
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/service_tokens", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := []AccessServiceToken{AccessServiceToken{
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+		ID:        "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		Name:      "CI/CD token",
+		ClientID:  "88bf3b6d86161464f6509f7219099e57.access.example.com",
+	}}
+
+	actual, _, err := client.AccessServiceTokens("01a7362d577a6c3019a474fd6f485823")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestCreateAccessServiceToken(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"name": "CI/CD token",
+				"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
+				"client_secret": "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/service_tokens", handler)
+
+	expected := AccessServiceTokenCreateResponse{
+		CreatedAt:    &createdAt,
+		UpdatedAt:    &updatedAt,
+		ID:           "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		Name:         "CI/CD token",
+		ClientID:     "88bf3b6d86161464f6509f7219099e57.access.example.com",
+		ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
+	}
+
+	actual, err := client.CreateAccessServiceToken("01a7362d577a6c3019a474fd6f485823", "CI/CD token")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}
+
+func TestUpdateAccessServiceToken(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "PUT", "Expected method 'PUT', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"name": "CI/CD token",
+				"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	expected := AccessServiceTokenUpdateResponse{
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+		ID:        "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		Name:      "CI/CD token",
+		ClientID:  "88bf3b6d86161464f6509f7219099e57.access.example.com",
+	}
+
+	actual, err := client.UpdateAccessServiceToken(
+		"01a7362d577a6c3019a474fd6f485823",
+		"f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		"CI/CD token",
+	)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}
+
+func TestDeleteAccessServiceToken(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, "DELETE", "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"name": "CI/CD token",
+				"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	expected := AccessServiceTokenUpdateResponse{
+		CreatedAt: &createdAt,
+		UpdatedAt: &updatedAt,
+		ID:        "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		Name:      "CI/CD token",
+		ClientID:  "88bf3b6d86161464f6509f7219099e57.access.example.com",
+	}
+
+	actual, err := client.DeleteAccessServiceToken(
+		"01a7362d577a6c3019a474fd6f485823",
+		"f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+	)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}