Repository: cloudflare/cloudflare-go
Pull request #1120 (2022-10-27 18:54:47)

AUTH-4694 Support Access service token rotation
## Description

Adds the `RotateAccessServiceToken` function, which is a wrapper for https://api.cloudflare.com/#access-service-tokens-rotate-a-service-token

## Has your change been tested?

Automated tests + I tried using it to rotate my own service tokens

## 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.
- [ ] 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.
- [X] This change is using publicly documented (api.cloudflare.com or developers.cloudflare.com) and stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


codecov-commenter: # [Codecov](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) Report
> Merging [#1120](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) (2179e37) into [master](https://codecov.io/gh/cloudflare/cloudflare-go/commit/e39278eab70d99b8cee194cc1ef12f4b4b8611ed?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) (e39278e) will **increase** coverage by `0.16%`.
> The diff coverage is `55.94%`.

```diff
@@            Coverage Diff             @@
##           master    #1120      +/-   ##
==========================================
+ Coverage   49.94%   50.11%   +0.16%     
==========================================
  Files         115      121       +6     
  Lines       10991    11722     +731     
==========================================
+ Hits         5490     5874     +384     
- Misses       4338     4565     +227     
- Partials     1163     1283     +120     
```


| [Impacted Files](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | Coverage Δ | |
|---|---|---|
| [access\_organization.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX29yZ2FuaXphdGlvbi5nbw==) | `64.70% <ø> (ø)` | |
| [cloudflare\_experimental.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-Y2xvdWRmbGFyZV9leHBlcmltZW50YWwuZ28=) | `0.00% <0.00%> (ø)` | |
| [device\_posture\_rule.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-ZGV2aWNlX3Bvc3R1cmVfcnVsZS5nbw==) | `61.80% <ø> (ø)` | |
| [intelligence\_asn.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-aW50ZWxsaWdlbmNlX2Fzbi5nbw==) | `64.70% <ø> (ø)` | |
| [intelligence\_domain.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-aW50ZWxsaWdlbmNlX2RvbWFpbi5nbw==) | `64.70% <ø> (ø)` | |
| [pages\_project.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-cGFnZXNfcHJvamVjdC5nbw==) | `50.81% <ø> (ø)` | |
| [rulesets.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-cnVsZXNldHMuZ28=) | `35.09% <ø> (ø)` | |
| [stream.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-c3RyZWFtLmdv) | `64.91% <ø> (ø)` | |
| [sts.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-c3RzLmdv) | `0.00% <0.00%> (ø)` | |
| [teams\_accounts.go](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-dGVhbXNfYWNjb3VudHMuZ28=) | `53.84% <ø> (ø)` | |
| ... and [30 more](https://codecov.io/gh/cloudflare/cloudflare-go/pull/1120/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare)

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/1120.txt b/.changelog/1120.txt
new file mode 100644
index 000000000..ca0c26758
--- /dev/null
+++ b/.changelog/1120.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+access: add support for service token rotation
+```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index daa1a382b..999e1a6c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,4 @@
-## 0.54.0 (Unreleased)
-
-## 0.53.0 (October 26th, 2022)
+## 0.53.0 (Unreleased)
 
 BREAKING CHANGES:
 
diff --git a/access_service_tokens.go b/access_service_tokens.go
index da3debf22..d0f5314f2 100644
--- a/access_service_tokens.go
+++ b/access_service_tokens.go
@@ -54,6 +54,18 @@ type AccessServiceTokenCreateResponse struct {
 	ClientSecret string     `json:"client_secret"`
 }
 
+// AccessServiceTokenRotateResponse is the same API response as the Create
+// operation.
+type AccessServiceTokenRotateResponse struct {
+	CreatedAt    *time.Time `json:"created_at"`
+	UpdatedAt    *time.Time `json:"updated_at"`
+	ExpiresAt    *time.Time `json:"expires_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 {
@@ -98,6 +110,15 @@ type AccessServiceTokensRefreshDetailResponse struct {
 	Result   AccessServiceTokenRefreshResponse `json:"result"`
 }
 
+// AccessServiceTokensRotateSecretDetailResponse is the API response, containing a
+// single Access Service Token.
+type AccessServiceTokensRotateSecretDetailResponse struct {
+	Success  bool                             `json:"success"`
+	Errors   []string                         `json:"errors"`
+	Messages []string                         `json:"messages"`
+	Result   AccessServiceTokenRotateResponse `json:"result"`
+}
+
 // AccessServiceTokens returns all Access Service Tokens for an account.
 //
 // API reference: https://api.cloudflare.com/#access-service-tokens-list-access-service-tokens
@@ -254,3 +275,23 @@ func (api *API) RefreshAccessServiceToken(ctx context.Context, rc *ResourceConta
 
 	return accessServiceTokenRefresh.Result, nil
 }
+
+// RotateAccessServiceToken rotates the client secret of an Access Service
+// Token in place.
+// API reference: https://api.cloudflare.com/#access-service-tokens-rotate-a-service-token
+func (api *API) RotateAccessServiceToken(ctx context.Context, rc *ResourceContainer, id string) (AccessServiceTokenRotateResponse, error) {
+	uri := fmt.Sprintf("/%s/%s/access/service_tokens/%s/rotate", rc.Level, rc.Identifier, id)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+	if err != nil {
+		return AccessServiceTokenRotateResponse{}, err
+	}
+
+	var accessServiceTokenRotate AccessServiceTokensRotateSecretDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenRotate)
+	if err != nil {
+		return AccessServiceTokenRotateResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return accessServiceTokenRotate.Result, nil
+}


Test patch:
diff --git a/access_service_tokens_test.go b/access_service_tokens_test.go
index 391a88b92..cbaa69c46 100644
--- a/access_service_tokens_test.go
+++ b/access_service_tokens_test.go
@@ -208,6 +208,49 @@ func TestRefreshAccessServiceToken(t *testing.T) {
 	}
 }
 
+func TestRotateAccessServiceToken(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": {
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"expires_at": "2015-01-01T05:20:00.12345Z",
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"name": "CI/CD token",
+				"client_id": "88bf3b6d86161464f6509f7219099e57.access.example.com",
+				"client_secret": "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5"
+			}
+		}
+		`)
+	}
+
+	expected := AccessServiceTokenRotateResponse{
+		CreatedAt:    &createdAt,
+		UpdatedAt:    &updatedAt,
+		ExpiresAt:    &expiresAt,
+		ID:           "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		Name:         "CI/CD token",
+		ClientID:     "88bf3b6d86161464f6509f7219099e57.access.example.com",
+		ClientSecret: "bdd31cbc4dec990953e39163fbbb194c93313ca9f0a6e420346af9d326b1d2a5",
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/service_tokens/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/rotate", handler)
+
+	actual, err := client.RotateAccessServiceToken(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}
+
 func TestDeleteAccessServiceToken(t *testing.T) {
 	setup()
 	defer teardown()