Repository: xanzy/go-gitlab
Pull request #1956 (2024-06-10 18:51:56)

Add support for rotating/revoking currently authenticated personal access token
Relates to #1686.

This PR does the following:

1. Adds `RotatePersonalAccessTokenSelf` for invoking the [`POST /personal_access_tokens/self/rotate` endpoint](https://docs.gitlab.com/ee/api/personal_access_tokens.html#use-a-request-header);
2. Adds `RevokePersonalAccessTokenSelf` for invoking the [`DELETE /personal_access_tokens/self` endpoint](https://docs.gitlab.com/ee/api/personal_access_tokens.html#using-a-request-header-1);
3. Renames the ambiguous `RotatePersonalAccessToken` to `RotatePersonalAccessTokenByID`;
4. Renames the ambiguous `RevokePersonalAccessToken` to `RevokePersonalAccessTokenByID`;
5. Adds a backwards-compatibility shim `RotatePersonalAccessToken` that calls `RotatePersonalAccessTokenByID`; and,
6. Adds a backwards-compatibility shim `RevokePersonalAccessToken` that calls `RevokePersonalAccessTokenByID`.


Patch:
diff --git a/personal_access_tokens.go b/personal_access_tokens.go
index ba32bd8..7c4adc4 100644
--- a/personal_access_tokens.go
+++ b/personal_access_tokens.go
@@ -129,12 +129,17 @@ type RotatePersonalAccessTokenOptions struct {
 	ExpiresAt *ISOTime `url:"expires_at,omitempty" json:"expires_at,omitempty"`
 }
 
-// RotatePersonalAccessToken revokes a token and returns a new token that
+// RotatePersonalAccessToken is a backwards-compat shim for RotatePersonalAccessTokenByID.
+func (s *PersonalAccessTokensService) RotatePersonalAccessToken(token int, opt *RotatePersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
+	return s.RotatePersonalAccessTokenByID(token, opt, options...)
+}
+
+// RotatePersonalAccessTokenByID revokes a token and returns a new token that
 // expires in one week per default.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/personal_access_tokens.html#rotate-a-personal-access-token
-func (s *PersonalAccessTokensService) RotatePersonalAccessToken(token int, opt *RotatePersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
+// https://docs.gitlab.com/ee/api/personal_access_tokens.html#use-a-personal-access-token-id
+func (s *PersonalAccessTokensService) RotatePersonalAccessTokenByID(token int, opt *RotatePersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
 	u := fmt.Sprintf("personal_access_tokens/%d/rotate", token)
 
 	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
@@ -151,11 +156,38 @@ func (s *PersonalAccessTokensService) RotatePersonalAccessToken(token int, opt *
 	return pat, resp, nil
 }
 
-// RevokePersonalAccessToken revokes a personal access token.
+// RotatePersonalAccessTokenSelf revokes the currently authenticated token
+// and returns a new token that expires in one week per default.
 //
 // GitLab API docs:
-// https://docs.gitlab.com/ee/api/personal_access_tokens.html#revoke-a-personal-access-token
+// https://docs.gitlab.com/ee/api/personal_access_tokens.html#use-a-request-header
+func (s *PersonalAccessTokensService) RotatePersonalAccessTokenSelf(opt *RotatePersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
+	u := "personal_access_tokens/self/rotate"
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pat := new(PersonalAccessToken)
+	resp, err := s.client.Do(req, pat)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pat, resp, nil
+}
+
+// RevokePersonalAccessToken is a backwards-compat shim for RevokePersonalAccessTokenByID.
 func (s *PersonalAccessTokensService) RevokePersonalAccessToken(token int, options ...RequestOptionFunc) (*Response, error) {
+	return s.RevokePersonalAccessTokenByID(token, options...)
+}
+
+// RevokePersonalAccessTokenByID revokes a personal access token by its ID.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/personal_access_tokens.html#using-a-personal-access-token-id-1
+func (s *PersonalAccessTokensService) RevokePersonalAccessTokenByID(token int, options ...RequestOptionFunc) (*Response, error) {
 	u := fmt.Sprintf("personal_access_tokens/%d", token)
 
 	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
@@ -165,3 +197,19 @@ func (s *PersonalAccessTokensService) RevokePersonalAccessToken(token int, optio
 
 	return s.client.Do(req, nil)
 }
+
+// RevokePersonalAccessTokenSelf revokes the currently authenticated
+// personal access token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/personal_access_tokens.html#using-a-request-header-1
+func (s *PersonalAccessTokensService) RevokePersonalAccessTokenSelf(options ...RequestOptionFunc) (*Response, error) {
+	u := "personal_access_tokens/self"
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}


Test patch:
diff --git a/personal_access_tokens_test.go b/personal_access_tokens_test.go
index 1236b76..de9bcf6 100644
--- a/personal_access_tokens_test.go
+++ b/personal_access_tokens_test.go
@@ -31,7 +31,9 @@ func TestListPersonalAccessTokensWithUserFilter(t *testing.T) {
 		mustWriteHTTPResponse(t, w, "testdata/list_personal_access_tokens_with_user_filter.json")
 	})
 
-	personalAccessTokens, _, err := client.PersonalAccessTokens.ListPersonalAccessTokens(&ListPersonalAccessTokensOptions{UserID: Ptr(1), ListOptions: ListOptions{Page: 1, PerPage: 10}})
+	personalAccessTokens, _, err := client.PersonalAccessTokens.ListPersonalAccessTokens(
+		&ListPersonalAccessTokensOptions{UserID: Ptr(1), ListOptions: ListOptions{Page: 1, PerPage: 10}},
+	)
 	if err != nil {
 		t.Errorf("PersonalAccessTokens.ListPersonalAccessTokens returned error: %v", err)
 	}
@@ -81,7 +83,9 @@ func TestListPersonalAccessTokensWithUserFilter(t *testing.T) {
 	}
 
 	if !reflect.DeepEqual(want, personalAccessTokens) {
-		t.Errorf("PersonalAccessTokens.ListPersonalAccessTokens returned %+v, want %+v", personalAccessTokens, want)
+		t.Errorf(
+			"PersonalAccessTokens.ListPersonalAccessTokens returned %+v, want %+v", personalAccessTokens, want,
+		)
 	}
 }
 
@@ -93,7 +97,9 @@ func TestListPersonalAccessTokensNoUserFilter(t *testing.T) {
 		mustWriteHTTPResponse(t, w, "testdata/list_personal_access_tokens_without_user_filter.json")
 	})
 
-	personalAccessTokens, _, err := client.PersonalAccessTokens.ListPersonalAccessTokens(&ListPersonalAccessTokensOptions{ListOptions: ListOptions{Page: 1, PerPage: 10}})
+	personalAccessTokens, _, err := client.PersonalAccessTokens.ListPersonalAccessTokens(
+		&ListPersonalAccessTokensOptions{ListOptions: ListOptions{Page: 1, PerPage: 10}},
+	)
 	if err != nil {
 		t.Errorf("PersonalAccessTokens.ListPersonalAccessTokens returned error: %v", err)
 	}
@@ -143,7 +149,9 @@ func TestListPersonalAccessTokensNoUserFilter(t *testing.T) {
 	}
 
 	if !reflect.DeepEqual(want, personalAccessTokens) {
-		t.Errorf("PersonalAccessTokens.ListPersonalAccessTokens returned %+v, want %+v", personalAccessTokens, want)
+		t.Errorf(
+			"PersonalAccessTokens.ListPersonalAccessTokens returned %+v, want %+v", personalAccessTokens, want,
+		)
 	}
 }
 
@@ -253,7 +261,75 @@ func TestRotatePersonalAccessToken(t *testing.T) {
 	}
 
 	if !reflect.DeepEqual(want, rotatedToken) {
-		t.Errorf("PersonalAccessTokens.RotatePersonalAccessTokens returned %+v, want %+v", rotatedToken, want)
+		t.Errorf(
+			"PersonalAccessTokens.RotatePersonalAccessToken returned %+v, want %+v", rotatedToken, want,
+		)
+	}
+}
+
+func TestRotatePersonalAccessTokenByID(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/personal_access_tokens/42/rotate", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		mustWriteHTTPResponse(t, w, "testdata/rotate_personal_access_token.json")
+	})
+
+	createdAt, _ := time.Parse(time.RFC3339, "2023-08-01T15:00:00.000Z")
+	expiration := ISOTime(time.Date(2023, time.August, 15, 0, 0, 0, 0, time.UTC))
+	opts := &RotatePersonalAccessTokenOptions{ExpiresAt: &expiration}
+	rotatedToken, _, err := client.PersonalAccessTokens.RotatePersonalAccessTokenByID(42, opts)
+	if err != nil {
+		t.Errorf("PersonalAccessTokens.RotatePersonalAccessTokenByID returned error: %v", err)
+	}
+
+	want := &PersonalAccessToken{
+		ID:        42,
+		UserID:    1337,
+		Name:      "Rotated Token",
+		Scopes:    []string{"api"},
+		ExpiresAt: &expiration,
+		CreatedAt: &createdAt,
+		Active:    true,
+		Revoked:   false,
+		Token:     "s3cr3t",
+	}
+
+	if !reflect.DeepEqual(want, rotatedToken) {
+		t.Errorf(
+			"PersonalAccessTokens.RotatePersonalAccessTokenByID returned %+v, want %+v", rotatedToken, want,
+		)
+	}
+}
+
+func TestRotatePersonalAccessTokenSelf(t *testing.T) {
+	mux, client := setup(t)
+	mux.HandleFunc("/api/v4/personal_access_tokens/self/rotate", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		mustWriteHTTPResponse(t, w, "testdata/rotate_personal_access_token.json")
+	})
+
+	createdAt, _ := time.Parse(time.RFC3339, "2023-08-01T15:00:00.000Z")
+	expiration := ISOTime(time.Date(2023, time.August, 15, 0, 0, 0, 0, time.UTC))
+	opts := &RotatePersonalAccessTokenOptions{ExpiresAt: &expiration}
+	rotatedToken, _, err := client.PersonalAccessTokens.RotatePersonalAccessTokenSelf(opts)
+	if err != nil {
+		t.Errorf("PersonalAccessTokens.RotatePersonalAccessTokenSelf returned error: %v", err)
+	}
+
+	want := &PersonalAccessToken{
+		ID:        42,
+		UserID:    1337,
+		Name:      "Rotated Token",
+		Scopes:    []string{"api"},
+		ExpiresAt: &expiration,
+		CreatedAt: &createdAt,
+		Active:    true,
+		Revoked:   false,
+		Token:     "s3cr3t",
+	}
+
+	if !reflect.DeepEqual(want, rotatedToken) {
+		t.Errorf("PersonalAccessTokens.RotatePersonalAccessTokenSelf returned %+v, want %+v", rotatedToken, want)
 	}
 }
 
@@ -269,3 +345,29 @@ func TestRevokePersonalAccessToken(t *testing.T) {
 		t.Errorf("PersonalAccessTokens.RevokePersonalAccessToken returned error: %v", err)
 	}
 }
+
+func TestRevokePersonalAccessTokenByID(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/personal_access_tokens/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.PersonalAccessTokens.RevokePersonalAccessTokenByID(1)
+	if err != nil {
+		t.Errorf("PersonalAccessTokens.RevokePersonalAccessTokenByID returned error: %v", err)
+	}
+}
+
+func TestRevokePersonalAccessTokenSelf(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/personal_access_tokens/self", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.PersonalAccessTokens.RevokePersonalAccessTokenSelf()
+	if err != nil {
+		t.Errorf("PersonalAccessTokens.RevokePersonalAccessTokenSelf returned error: %v", err)
+	}
+}