Repository: xanzy/go-gitlab
Pull request #1842 (2023-12-05 04:01:01)

add support for service account and personal access tokens handler.
Add the ability to handle creating a group service account, add a personal access token and rotate the personal access token.

Tests:
1. CreateServiceAccount
2. AddServiceAccountsPAT
3. RotateServiceAccountsPAT

Group Service Account were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407775) in GitLab 16.1



Patch:
diff --git a/group_serviceaccounts.go b/group_serviceaccounts.go
new file mode 100644
index 0000000..4acb0b1
--- /dev/null
+++ b/group_serviceaccounts.go
@@ -0,0 +1,118 @@
+//
+// Copyright 2023, James Hong
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// GroupServiceAccount represents a GitLab service account user.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#create-service-account-user
+type GroupServiceAccount struct {
+	ID       int    `json:"id"`
+	Name     string `json:"name"`
+	UserName string `json:"username"`
+}
+
+// CreateServiceAccount create a new service account user for a group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#create-service-account-user
+func (s *GroupsService) CreateServiceAccount(gid interface{}, options ...RequestOptionFunc) (*GroupServiceAccount, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	sa := new(GroupServiceAccount)
+	resp, err := s.client.Do(req, sa)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return sa, resp, nil
+}
+
+// CreateServiceAccountPersonalAccessTokenOptions represents the available
+// CreateServiceAccountPersonalAccessToken() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#create-personal-access-token-for-service-account-user
+type CreateServiceAccountPersonalAccessTokenOptions struct {
+	Scopes *[]string `url:"scopes,omitempty" json:"scopes,omitempty"`
+	Name   *string   `url:"name,omitempty" json:"name,omitempty"`
+}
+
+// CreateServiceAccountPersonalAccessToken add a new Personal Access Token for a
+// service account user for a group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/groups.html#create-personal-access-token-for-service-account-user
+func (s *GroupsService) CreateServiceAccountPersonalAccessToken(gid interface{}, serviceAccount int, opt *CreateServiceAccountPersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts/%d/personal_access_tokens", PathEscape(group), serviceAccount)
+
+	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
+}
+
+// RotateServiceAccountPersonalAccessToken rotates a Personal Access Token for a
+// service account user for a group.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/groups.html#create-personal-access-token-for-service-account-user
+func (s *GroupsService) RotateServiceAccountPersonalAccessToken(gid interface{}, serviceAccount, token int, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts/%d/personal_access_tokens/%d/rotate", PathEscape(group), serviceAccount, token)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, nil, 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
+}


Test patch:
diff --git a/group_serviceaccounts_test.go b/group_serviceaccounts_test.go
new file mode 100644
index 0000000..e4793f6
--- /dev/null
+++ b/group_serviceaccounts_test.go
@@ -0,0 +1,149 @@
+//
+// Copyright 2023, James Hong
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestCreateServiceAccount(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/service_accounts", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprint(w, `
+      {
+	      "id": 57,
+	      "username": "service_account_group_345_6018816a18e515214e0c34c2b33523fc",
+	      "name": "Service account user"
+      }`)
+	})
+
+	sa, _, err := client.Groups.CreateServiceAccount(1)
+	if err != nil {
+		t.Error(err)
+	}
+
+	want := &GroupServiceAccount{
+		ID:       57,
+		UserName: "service_account_group_345_6018816a18e515214e0c34c2b33523fc",
+		Name:     "Service account user",
+	}
+
+	if !reflect.DeepEqual(sa, want) {
+		t.Errorf("CreateServiceAccount returned \ngot:\n%v\nwant:\n%v", Stringify(sa), Stringify(want))
+	}
+}
+
+func TestCreateServiceAccountPersonalAccessToken(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/service_accounts/57/personal_access_tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprint(w, `
+      {
+      	"id":6,
+      	"name":"service_account_token",
+      	"revoked":false,
+      	"created_at":"2023-06-13T07:47:13.000Z",
+      	"scopes":["api"],
+      	"user_id":71,
+      	"last_used_at":null,
+      	"active":true,
+      	"expires_at":"2024-06-12",
+      	"token":"random_token"
+      }`)
+	})
+	options := &CreateServiceAccountPersonalAccessTokenOptions{
+		Scopes: Ptr([]string{"api"}),
+		Name:   Ptr("service_account_token"),
+	}
+	pat, _, err := client.Groups.CreateServiceAccountPersonalAccessToken(1, 57, options)
+	if err != nil {
+		t.Error(err)
+	}
+
+	datePointer := time.Date(2023, 0o6, 13, 0o7, 47, 13, 0, time.UTC)
+	expiresAt := ISOTime(time.Date(2024, time.June, 12, 0, 0, 0, 0, time.UTC))
+
+	want := &PersonalAccessToken{
+		ID:         6,
+		Name:       "service_account_token",
+		Revoked:    false,
+		CreatedAt:  &datePointer,
+		Scopes:     []string{"api"},
+		UserID:     71,
+		LastUsedAt: nil,
+		Active:     true,
+		ExpiresAt:  &expiresAt,
+		Token:      "random_token",
+	}
+
+	if !reflect.DeepEqual(pat, want) {
+		t.Errorf("CreateServiceAccountPersonalAccessToken returned \ngot:\n%v\nwant:\n%v", Stringify(pat), Stringify(want))
+	}
+}
+
+func TestRotateServiceAccountPersonalAccessToken(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/groups/1/service_accounts/57/personal_access_tokens/6/rotate", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprint(w, `
+      {
+      	"id":7,
+      	"name":"service_account_token",
+      	"revoked":false,
+      	"created_at":"2023-06-13T07:54:49.000Z",
+      	"scopes":["api"],
+      	"user_id":71,
+      	"last_used_at":null,
+      	"active":true,
+      	"expires_at":"2025-06-20",
+      	"token":"random_token_2"
+      }`)
+	})
+
+	pat, _, err := client.Groups.RotateServiceAccountPersonalAccessToken(1, 57, 6)
+	if err != nil {
+		t.Error(err)
+	}
+
+	datePointer := time.Date(2023, 0o6, 13, 0o7, 54, 49, 0, time.UTC)
+	expiresAt := ISOTime(time.Date(2025, time.June, 20, 0, 0, 0, 0, time.UTC))
+
+	want := &PersonalAccessToken{
+		ID:         7,
+		Name:       "service_account_token",
+		Revoked:    false,
+		CreatedAt:  &datePointer,
+		Scopes:     []string{"api"},
+		UserID:     71,
+		LastUsedAt: nil,
+		Active:     true,
+		ExpiresAt:  &expiresAt,
+		Token:      "random_token_2",
+	}
+
+	if !reflect.DeepEqual(pat, want) {
+		t.Errorf("RotateServiceAccountPersonalAccessToken returned \ngot:\n%v\nwant:\n%v", Stringify(pat), Stringify(want))
+	}
+}