Repository: xanzy/go-gitlab
Pull request #1751 (2023-07-11 00:13:49)

Add Support for Job Token Scope APIs
With GitLab 16.0, CI_JOB_TOKEN now enforces the use of the project allow list. In order to add support for that to the terraform provider, we need to add the APIs to `go-gitlab`.

This PR will add support for those APIs, documented here: https://docs.gitlab.com/ee/api/project_job_token_scopes.html


Patch:
diff --git a/gitlab.go b/gitlab.go
index 9f46b5c..2f646f5 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -153,6 +153,7 @@ type Client struct {
 	Issues                       *IssuesService
 	IssuesStatistics             *IssuesStatisticsService
 	Jobs                         *JobsService
+	JobTokenScope                *JobTokenScopeService
 	Keys                         *KeysService
 	Labels                       *LabelsService
 	License                      *LicenseService
@@ -370,6 +371,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Issues = &IssuesService{client: c, timeStats: timeStats}
 	c.IssuesStatistics = &IssuesStatisticsService{client: c}
 	c.Jobs = &JobsService{client: c}
+	c.JobTokenScope = &JobTokenScopeService{client: c}
 	c.Keys = &KeysService{client: c}
 	c.Labels = &LabelsService{client: c}
 	c.License = &LicenseService{client: c}
diff --git a/job_token_scope.go b/job_token_scope.go
new file mode 100644
index 0000000..82dc508
--- /dev/null
+++ b/job_token_scope.go
@@ -0,0 +1,125 @@
+// Copyright 2021, Sander van Harmelen
+//
+// 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"
+)
+
+// JobTokenScopeService handles communication with project CI settings
+// such as token permissions.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/project_job_token_scopes.html
+type JobTokenScopeService struct {
+	client *Client
+}
+
+// JobTokenInboundAllowItem represents a single job token inbound allowlist item.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/project_job_token_scopes.html
+type JobTokenInboundAllowItem struct {
+	SourceProjectID int `json:"source_project_id"`
+	TargetProjectID int `json:"target_project_id"`
+}
+
+// GetJobTokenInboundAllowListOptions represents the available
+// GetJobTokenInboundAllowList() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_job_token_scopes.html#get-a-projects-cicd-job-token-inbound-allowlist
+type GetJobTokenInboundAllowListOptions struct {
+	ListOptions
+}
+
+// GetProjectJobTokenInboundAllowList fetches the CI/CD job token inbound
+// allowlist (job token scope) of a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_job_token_scopes.html#get-a-projects-cicd-job-token-inbound-allowlist
+func (j *JobTokenScopeService) GetProjectJobTokenInboundAllowList(pid interface{}, opt *GetJobTokenInboundAllowListOptions, options ...RequestOptionFunc) ([]*Project, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf(`projects/%s/job_token_scope/allowlist`, PathEscape(project))
+
+	req, err := j.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var ps []*Project
+	resp, err := j.client.Do(req, &ps)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ps, resp, nil
+}
+
+// AddProjectToJobScopeAllowListOptions represents the available
+// AddProjectToJobScopeAllowList() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_job_token_scopes.html#create-a-new-project-to-a-projects-cicd-job-token-inbound-allowlist
+type JobTokenInboundAllowOptions struct {
+	TargetProjectID *int `url:"target_project_id,omitempty" json:"target_project_id,omitempty"`
+}
+
+// AddProjectToJobScopeAllowList adds a new project to a project's job token
+// inbound allow list.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_job_token_scopes.html#create-a-new-project-to-a-projects-cicd-job-token-inbound-allowlist
+func (j *JobTokenScopeService) AddProjectToJobScopeAllowList(pid interface{}, opt *JobTokenInboundAllowOptions, options ...RequestOptionFunc) (*JobTokenInboundAllowItem, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf(`projects/%s/job_token_scope/allowlist`, PathEscape(project))
+
+	req, err := j.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ai := new(JobTokenInboundAllowItem)
+	resp, err := j.client.Do(req, ai)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ai, resp, nil
+}
+
+// RemoveProjectFromJobScopeAllowList removes a project from a project's job
+// token inbound allow list.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_job_token_scopes.html#remove-a-project-from-a-projects-cicd-job-token-inbound-allowlist
+func (j *JobTokenScopeService) RemoveProjectFromJobScopeAllowList(pid interface{}, targetProject int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf(`projects/%s/job_token_scope/allowlist/%d`, PathEscape(project), targetProject)
+
+	req, err := j.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return j.client.Do(req, nil)
+}


Test patch:
diff --git a/job_token_scope_test.go b/job_token_scope_test.go
new file mode 100644
index 0000000..4427510
--- /dev/null
+++ b/job_token_scope_test.go
@@ -0,0 +1,121 @@
+// Copyright 2021, Sander van Harmelen
+//
+// 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 (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// This tests that when calling the GetProjectJobTokenInboundAllowList, we get a
+// list of projects back properly. There isn't a "deep" test with every attribute
+// specifieid, because the object returned is a *Project object, which is already
+// tested in project.go.
+func TestGetProjectJobTokenInboundAllowList(t *testing.T) {
+	mux, client := setup(t)
+
+	// Handle project ID 1, and print a result of two projects
+	mux.HandleFunc("/api/v4/projects/1/job_token_scope/allowlist", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+
+		// Print on the response
+		fmt.Fprint(w, `[{"id":1},{"id":2}]`)
+	})
+
+	want := []*Project{{ID: 1}, {ID: 2}}
+	projects, _, err := client.JobTokenScope.GetProjectJobTokenInboundAllowList(
+		1,
+		&GetJobTokenInboundAllowListOptions{},
+	)
+
+	assert.NoError(t, err)
+	assert.Equal(t, want, projects)
+}
+
+func TestAddProjectToJobScopeAllowList(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/job_token_scope/allowlist", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+
+		// Read the request to determine which target project is passed in
+		body, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatalf("JobTokenScope.AddProjectToJobScopeAllowList failed to read body")
+		}
+
+		// Parse to object to ensure it's sent on the request appropriately.
+		var createTokenRequest JobTokenInboundAllowOptions
+		err = json.Unmarshal(body, &createTokenRequest)
+		if err != nil {
+			t.Fatalf("JobTokenScope.AddProjectToJobScopeAllowList failed to unmarshal body: %v", err)
+		}
+
+		// Ensure we provide the proper response
+		w.WriteHeader(http.StatusCreated)
+
+		// Print on the response with the proper target project
+		fmt.Fprintf(w, `{
+			"source_project_id": 1,
+			"target_project_id": %d
+		}`, *createTokenRequest.TargetProjectID)
+	})
+
+	want := &JobTokenInboundAllowItem{
+		SourceProjectID: 1,
+		TargetProjectID: 2,
+	}
+
+	addTokenResponse, resp, err := client.JobTokenScope.AddProjectToJobScopeAllowList(
+		1,
+		&JobTokenInboundAllowOptions{TargetProjectID: Int(2)},
+	)
+	assert.NoError(t, err)
+	assert.Equal(t, want, addTokenResponse)
+	assert.Equal(t, 201, resp.StatusCode)
+}
+
+func TestRemoveProjectFromJobScopeAllowList(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/job_token_scope/allowlist/2", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+
+		// Read the request to determine which target project is passed in
+		body, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatalf("JobTokenScope.RemoveProjectFromJobScopeAllowList failed to read body")
+		}
+
+		// The body should be empty since all attributes are passed in the path
+		if body != nil && string(body) != "" {
+			t.Fatalf("JobTokenScope.RemoveProjectFromJobScopeAllowList failed to unmarshal body: %v", err)
+		}
+
+		// Ensure we provide the proper response
+		w.WriteHeader(http.StatusNoContent)
+
+		// Print an empty body, since that's what the API provides.
+		fmt.Fprint(w, "")
+	})
+
+	resp, err := client.JobTokenScope.RemoveProjectFromJobScopeAllowList(1, 2)
+	assert.NoError(t, err)
+	assert.Equal(t, 204, resp.StatusCode)
+}