diff --git a/docs/content/docs/providers/bitbucket-datacenter.md b/docs/content/docs/providers/bitbucket-datacenter.md index 910769991..f78700339 100644 --- a/docs/content/docs/providers/bitbucket-datacenter.md +++ b/docs/content/docs/providers/bitbucket-datacenter.md @@ -8,22 +8,22 @@ This page covers how to configure Pipelines-as-Code with [Bitbucket Data Center] ## Prerequisites - A running Pipelines-as-Code [installation]({{< relref "/docs/installation/installation" >}}) -- A Bitbucket Data Center personal access token with `PROJECT_ADMIN` and `REPOSITORY_ADMIN` permissions (see below) +- A Bitbucket Data Center HTTP access token with `PROJECT_ADMIN` or `REPOSITORY_ADMIN` permissions (see below) - The public URL of your Pipelines-as-Code controller route or ingress endpoint -## Create a Bitbucket Personal Access Token +## Create a Bitbucket HTTP Access Token -Generate a personal access token as the manager of the project by following the steps here: +Generate a HTTP access token as the manager of the project or repository by following the steps here: -The token needs the `PROJECT_ADMIN` and `REPOSITORY_ADMIN` permissions. It also needs access to forked repositories in pull requests, otherwise Pipelines-as-Code cannot process and access the pull request. +The token needs the `PROJECT_ADMIN` or `REPOSITORY_ADMIN` permissions. It also needs admin access to forked repositories in pull requests, otherwise Pipelines-as-Code cannot process and access the pull request. {{< callout type="info" >}} -The service account user that owns the token must be a **licensed Bitbucket +When using a personal HTTP token, the associated user must be a **licensed Bitbucket user** (i.e., granted the `LICENSED_USER` global permission) for group-based -permission checks to work. If the service account is an unlicensed technical +permission checks to work. If the user account is an unlicensed technical user, group membership cannot be resolved and users with group-only access will not be able to trigger builds. As a workaround, add those users individually to the project or repository permissions. diff --git a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go index 0637d611e..9e34e987b 100644 --- a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go +++ b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go @@ -276,9 +276,6 @@ func removeLastSegment(urlStr string) string { } func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.Event, repo *v1alpha1.Repository, _ *events.EventEmitter) error { - if event.Provider.User == "" { - return fmt.Errorf("no spec.git_provider.user has been set in the repo crd") - } if event.Provider.Token == "" { return fmt.Errorf("no spec.git_provider.secret has been set in the repo crd") } @@ -317,14 +314,23 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E v.run = run v.repo = repo v.triggerEvent = event.EventType - _, resp, err := v.Client().Users.FindLogin(ctx, event.Provider.User) + + var resp *scm.Response + var err error + // we only need a valid token to access rest api + _, resp, err = v.Client().Users.Find(ctx) if resp != nil && resp.Status == http.StatusUnauthorized { - return fmt.Errorf("cannot get user %s with token: %w", event.Provider.User, err) + return fmt.Errorf("token validation failed: unauthorized") + } + if resp != nil && resp.Status == http.StatusInternalServerError { + return fmt.Errorf("token validation failed: Internal Server Error") } if err != nil { - return fmt.Errorf("cannot get user %s: %w", event.Provider.User, err) + return fmt.Errorf("token validation failed: http status: %d : %w", resp.Status, err) } + // the token must have admin permissions at project or repository level + return nil } diff --git a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go index 20ec221e9..f34b40bd1 100644 --- a/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go +++ b/pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1" "github.com/openshift-pipelines/pipelines-as-code/pkg/params" "github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients" "github.com/openshift-pipelines/pipelines-as-code/pkg/params/info" @@ -309,13 +310,15 @@ func TestSetClient(t *testing.T) { name string apiURL string opts *info.Event + repo *v1alpha1.Repository wantErrSubstr string + muxToken func(w http.ResponseWriter, r *http.Request) muxUser func(w http.ResponseWriter, r *http.Request) }{ { - name: "bad/no username", + name: "bad/no token", opts: info.NewEvent(), - wantErrSubstr: "no spec.git_provider.user", + wantErrSubstr: "no spec.git_provider.secret", }, { name: "bad/no secret", @@ -330,57 +333,125 @@ func TestSetClient(t *testing.T) { name: "bad/no url", opts: &info.Event{ Provider: &info.Provider{ - User: "foo", Token: "bar", }, }, wantErrSubstr: "no spec.git_provider.url", }, { - name: "bad/invalid user", + name: "bad/invalid user in whomi", + opts: &info.Event{ + Provider: &info.Provider{ + Token: "bar", + URL: "https://fakebitbucketdc", + }, + }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"errors": [{"message": "Unauthorized"}]}`)) + }, + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: unauthorized", + }, + { + name: "bad/invalid user at rest after whomi", opts: &info.Event{ Provider: &info.Provider{ User: "foo", Token: "bar", - URL: "https://foo.bar", + URL: "https://fakebitbucketdc", }, }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, `foo`) + }, muxUser: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"errors": [{"message": "Unauthorized"}]}`)) }, - apiURL: "https://foo.bar/rest", - wantErrSubstr: "cannot get user foo with token", + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: unauthorized", + }, + { + name: "internal error at whoami", + opts: &info.Event{ + Provider: &info.Provider{ + User: "foo", + Token: "bar", + URL: "https://fakebitbucketdc", + }, + }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: Internal Server Error", }, { - name: "bad/unknown error", + name: "not found at whoami", opts: &info.Event{ Provider: &info.Provider{ User: "foo", Token: "bar", - URL: "https://foo.bar", + URL: "https://fakebitbucketdc", }, }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: http status: 404 : ", + }, + { + name: "not found at whoami with error message", + opts: &info.Event{ + Provider: &info.Provider{ + User: "foo", + Token: "bar", + URL: "https://fakebitbucketdc", + }, + }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"errors": [{"message": "Not Found"}]}`)) + }, + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: http status: 404 : Not Found", + }, + { + name: "internal error at users rest", + opts: &info.Event{ + Provider: &info.Provider{ + User: "foo", + Token: "bar", + URL: "https://fakebitbucketdc", + }, + }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, `foo`) + }, muxUser: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"errors": [{"message": "Internal Server Error"}]}`)) }, - apiURL: "https://foo.bar/rest", - wantErrSubstr: "cannot get user foo: Internal Server Error", + apiURL: "https://fakebitbucketdc/rest", + wantErrSubstr: "token validation failed: Internal Server Error", }, { name: "good/url append /rest", opts: &info.Event{ Provider: &info.Provider{ - User: "foo", Token: "bar", - URL: "https://foo.bar", + URL: "https://fakebitbucketdc", }, }, + muxToken: func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, `foo`) + }, muxUser: func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, `{"name": "foo"}`) }, - apiURL: "https://foo.bar/rest", + apiURL: "https://fakebitbucketdc/rest", }, } for _, tt := range tests { @@ -395,11 +466,14 @@ func TestSetClient(t *testing.T) { ctx, _ := rtesting.SetupFakeContext(t) client, mux, tearDown, tURL := bbtest.SetupBBDataCenterClient() defer tearDown() + if tt.muxToken != nil { + mux.HandleFunc("/whoami", tt.muxToken) + } if tt.muxUser != nil { mux.HandleFunc("/users/foo", tt.muxUser) } v := &Provider{client: client, baseURL: tURL} - err := v.SetClient(ctx, fakeRun, tt.opts, nil, nil) + err := v.SetClient(ctx, fakeRun, tt.opts, tt.repo, nil) if tt.wantErrSubstr != "" { assert.ErrorContains(t, err, tt.wantErrSubstr) return diff --git a/pkg/provider/bitbucketdatacenter/test/test.go b/pkg/provider/bitbucketdatacenter/test/test.go index 10bfa67e8..0cfdb5cf3 100644 --- a/pkg/provider/bitbucketdatacenter/test/test.go +++ b/pkg/provider/bitbucketdatacenter/test/test.go @@ -21,8 +21,9 @@ import ( ) var ( - defaultAPIURL = "/rest/api/1.0" - buildAPIURL = "/rest/build-status/1.0" + defaultAPIURL = "/rest/api/1.0" + buildAPIURL = "/rest/build-status/1.0" + defaultApplinksURL = "/plugins/servlet/applinks" ) func SetupBBDataCenterClient() (*scm.Client, *http.ServeMux, func(), string) { @@ -30,6 +31,7 @@ func SetupBBDataCenterClient() (*scm.Client, *http.ServeMux, func(), string) { apiHandler := http.NewServeMux() apiHandler.Handle(defaultAPIURL+"/", http.StripPrefix(defaultAPIURL, mux)) apiHandler.Handle(buildAPIURL+"/", http.StripPrefix(buildAPIURL, mux)) + apiHandler.Handle(defaultApplinksURL+"/", http.StripPrefix(defaultApplinksURL, mux)) apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:") fmt.Fprintln(os.Stderr)