Skip to content

Commit cbd582d

Browse files
Ru13enzakisk
authored andcommitted
feat(bitbucketdatacenter): allow service accounts to not require user in the set client
1 parent fe709ae commit cbd582d

4 files changed

Lines changed: 117 additions & 30 deletions

File tree

docs/content/docs/providers/bitbucket-datacenter.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,27 @@ This page covers how to configure Pipelines-as-Code with [Bitbucket Data Center]
88
## Prerequisites
99

1010
- A running Pipelines-as-Code [installation]({{< relref "/docs/installation/installation" >}})
11-
- A Bitbucket Data Center personal access token with `PROJECT_ADMIN` and `REPOSITORY_ADMIN` permissions (see below)
11+
- A Bitbucket Data Center HTTP access token with `PROJECT_ADMIN` or `REPOSITORY_ADMIN` permissions (see below)
1212
- The public URL of your Pipelines-as-Code controller route or ingress endpoint
1313

14-
## Create a Bitbucket Personal Access Token
14+
## Create a Bitbucket HTTP Access Token
1515

16-
Generate a personal access token as the manager of the project by following the steps here:
16+
Generate an HTTP access token using one of the following token types:
1717

18-
<https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html>
18+
- Personal HTTP access token
19+
- Repository HTTP access token
20+
- Project HTTP access token
1921

20-
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.
22+
For detailed instructions on creating and managing access tokens, refer to the official Bitbucket Data Center documentation:
23+
<https://confluence.atlassian.com/bitbucketserver/http-access-tokens-939515499.html>
24+
25+
The token must have either `PROJECT_ADMIN` or `REPOSITORY_ADMIN` permissions. If Pipelines-as-Code needs to process pull requests from forked repositories, the token must also have administrative access to those forked repositories. Without this access, Pipelines-as-Code cannot retrieve the required pull request information or interact with the forked repository.
2126

2227
{{< callout type="info" >}}
2328

24-
The service account user that owns the token must be a **licensed Bitbucket
29+
When using a personal HTTP token, the associated user must be a **licensed Bitbucket
2530
user** (i.e., granted the `LICENSED_USER` global permission) for group-based
26-
permission checks to work. If the service account is an unlicensed technical
31+
permission checks to work. If the user account is an unlicensed technical
2732
user, group membership cannot be resolved and users with group-only access
2833
will not be able to trigger builds. As a workaround, add those users
2934
individually to the project or repository permissions.

pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,6 @@ func removeLastSegment(urlStr string) string {
276276
}
277277

278278
func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.Event, repo *v1alpha1.Repository, _ *events.EventEmitter) error {
279-
if event.Provider.User == "" {
280-
return fmt.Errorf("no spec.git_provider.user has been set in the repo crd")
281-
}
282279
if event.Provider.Token == "" {
283280
return fmt.Errorf("no spec.git_provider.secret has been set in the repo crd")
284281
}
@@ -317,14 +314,23 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E
317314
v.run = run
318315
v.repo = repo
319316
v.triggerEvent = event.EventType
320-
_, resp, err := v.Client().Users.FindLogin(ctx, event.Provider.User)
317+
318+
var resp *scm.Response
319+
var err error
320+
// we only need a valid token to access rest api
321+
_, resp, err = v.Client().Users.Find(ctx)
321322
if resp != nil && resp.Status == http.StatusUnauthorized {
322-
return fmt.Errorf("cannot get user %s with token: %w", event.Provider.User, err)
323+
return fmt.Errorf("token validation failed: unauthorized")
324+
}
325+
if resp != nil && resp.Status == http.StatusInternalServerError {
326+
return fmt.Errorf("token validation failed: Internal Server Error")
323327
}
324328
if err != nil {
325-
return fmt.Errorf("cannot get user %s: %w", event.Provider.User, err)
329+
return fmt.Errorf("token validation failed: http status: %d : %w", resp.Status, err)
326330
}
327331

332+
// the token must have admin permissions at project or repository level
333+
328334
return nil
329335
}
330336

pkg/provider/bitbucketdatacenter/bitbucketdatacenter_test.go

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"testing"
1616
"time"
1717

18+
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1819
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1920
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients"
2021
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
@@ -309,13 +310,15 @@ func TestSetClient(t *testing.T) {
309310
name string
310311
apiURL string
311312
opts *info.Event
313+
repo *v1alpha1.Repository
312314
wantErrSubstr string
315+
muxToken func(w http.ResponseWriter, r *http.Request)
313316
muxUser func(w http.ResponseWriter, r *http.Request)
314317
}{
315318
{
316-
name: "bad/no username",
319+
name: "bad/no token",
317320
opts: info.NewEvent(),
318-
wantErrSubstr: "no spec.git_provider.user",
321+
wantErrSubstr: "no spec.git_provider.secret",
319322
},
320323
{
321324
name: "bad/no secret",
@@ -330,57 +333,125 @@ func TestSetClient(t *testing.T) {
330333
name: "bad/no url",
331334
opts: &info.Event{
332335
Provider: &info.Provider{
333-
User: "foo",
334336
Token: "bar",
335337
},
336338
},
337339
wantErrSubstr: "no spec.git_provider.url",
338340
},
339341
{
340-
name: "bad/invalid user",
342+
name: "bad/invalid user in whomi",
343+
opts: &info.Event{
344+
Provider: &info.Provider{
345+
Token: "bar",
346+
URL: "https://fakebitbucketdc",
347+
},
348+
},
349+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
350+
w.WriteHeader(http.StatusUnauthorized)
351+
_, _ = w.Write([]byte(`{"errors": [{"message": "Unauthorized"}]}`))
352+
},
353+
apiURL: "https://fakebitbucketdc/rest",
354+
wantErrSubstr: "token validation failed: unauthorized",
355+
},
356+
{
357+
name: "bad/invalid user at rest after whomi",
341358
opts: &info.Event{
342359
Provider: &info.Provider{
343360
User: "foo",
344361
Token: "bar",
345-
URL: "https://foo.bar",
362+
URL: "https://fakebitbucketdc",
346363
},
347364
},
365+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
366+
fmt.Fprint(w, `foo`)
367+
},
348368
muxUser: func(w http.ResponseWriter, _ *http.Request) {
349369
w.WriteHeader(http.StatusUnauthorized)
350370
_, _ = w.Write([]byte(`{"errors": [{"message": "Unauthorized"}]}`))
351371
},
352-
apiURL: "https://foo.bar/rest",
353-
wantErrSubstr: "cannot get user foo with token",
372+
apiURL: "https://fakebitbucketdc/rest",
373+
wantErrSubstr: "token validation failed: unauthorized",
374+
},
375+
{
376+
name: "internal error at whoami",
377+
opts: &info.Event{
378+
Provider: &info.Provider{
379+
User: "foo",
380+
Token: "bar",
381+
URL: "https://fakebitbucketdc",
382+
},
383+
},
384+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
385+
w.WriteHeader(http.StatusInternalServerError)
386+
},
387+
apiURL: "https://fakebitbucketdc/rest",
388+
wantErrSubstr: "token validation failed: Internal Server Error",
354389
},
355390
{
356-
name: "bad/unknown error",
391+
name: "not found at whoami",
357392
opts: &info.Event{
358393
Provider: &info.Provider{
359394
User: "foo",
360395
Token: "bar",
361-
URL: "https://foo.bar",
396+
URL: "https://fakebitbucketdc",
362397
},
363398
},
399+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
400+
w.WriteHeader(http.StatusNotFound)
401+
},
402+
apiURL: "https://fakebitbucketdc/rest",
403+
wantErrSubstr: "token validation failed: http status: 404 : ",
404+
},
405+
{
406+
name: "not found at whoami with error message",
407+
opts: &info.Event{
408+
Provider: &info.Provider{
409+
User: "foo",
410+
Token: "bar",
411+
URL: "https://fakebitbucketdc",
412+
},
413+
},
414+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
415+
w.WriteHeader(http.StatusNotFound)
416+
_, _ = w.Write([]byte(`{"errors": [{"message": "Not Found"}]}`))
417+
},
418+
apiURL: "https://fakebitbucketdc/rest",
419+
wantErrSubstr: "token validation failed: http status: 404 : Not Found",
420+
},
421+
{
422+
name: "internal error at users rest",
423+
opts: &info.Event{
424+
Provider: &info.Provider{
425+
User: "foo",
426+
Token: "bar",
427+
URL: "https://fakebitbucketdc",
428+
},
429+
},
430+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
431+
fmt.Fprint(w, `foo`)
432+
},
364433
muxUser: func(w http.ResponseWriter, _ *http.Request) {
365434
w.WriteHeader(http.StatusInternalServerError)
366435
_, _ = w.Write([]byte(`{"errors": [{"message": "Internal Server Error"}]}`))
367436
},
368-
apiURL: "https://foo.bar/rest",
369-
wantErrSubstr: "cannot get user foo: Internal Server Error",
437+
apiURL: "https://fakebitbucketdc/rest",
438+
wantErrSubstr: "token validation failed: Internal Server Error",
370439
},
371440
{
372441
name: "good/url append /rest",
373442
opts: &info.Event{
374443
Provider: &info.Provider{
375-
User: "foo",
376444
Token: "bar",
377-
URL: "https://foo.bar",
445+
URL: "https://fakebitbucketdc",
378446
},
379447
},
448+
muxToken: func(w http.ResponseWriter, _ *http.Request) {
449+
fmt.Fprint(w, `foo`)
450+
},
380451
muxUser: func(w http.ResponseWriter, _ *http.Request) {
381452
fmt.Fprint(w, `{"name": "foo"}`)
382453
},
383-
apiURL: "https://foo.bar/rest",
454+
apiURL: "https://fakebitbucketdc/rest",
384455
},
385456
}
386457
for _, tt := range tests {
@@ -395,11 +466,14 @@ func TestSetClient(t *testing.T) {
395466
ctx, _ := rtesting.SetupFakeContext(t)
396467
client, mux, tearDown, tURL := bbtest.SetupBBDataCenterClient()
397468
defer tearDown()
469+
if tt.muxToken != nil {
470+
mux.HandleFunc("/whoami", tt.muxToken)
471+
}
398472
if tt.muxUser != nil {
399473
mux.HandleFunc("/users/foo", tt.muxUser)
400474
}
401475
v := &Provider{client: client, baseURL: tURL}
402-
err := v.SetClient(ctx, fakeRun, tt.opts, nil, nil)
476+
err := v.SetClient(ctx, fakeRun, tt.opts, tt.repo, nil)
403477
if tt.wantErrSubstr != "" {
404478
assert.ErrorContains(t, err, tt.wantErrSubstr)
405479
return

pkg/provider/bitbucketdatacenter/test/test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ import (
2121
)
2222

2323
var (
24-
defaultAPIURL = "/rest/api/1.0"
25-
buildAPIURL = "/rest/build-status/1.0"
24+
defaultAPIURL = "/rest/api/1.0"
25+
buildAPIURL = "/rest/build-status/1.0"
26+
defaultApplinksURL = "/plugins/servlet/applinks"
2627
)
2728

2829
func SetupBBDataCenterClient() (*scm.Client, *http.ServeMux, func(), string) {
2930
mux := http.NewServeMux()
3031
apiHandler := http.NewServeMux()
3132
apiHandler.Handle(defaultAPIURL+"/", http.StripPrefix(defaultAPIURL, mux))
3233
apiHandler.Handle(buildAPIURL+"/", http.StripPrefix(buildAPIURL, mux))
34+
apiHandler.Handle(defaultApplinksURL+"/", http.StripPrefix(defaultApplinksURL, mux))
3335
apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
3436
fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:")
3537
fmt.Fprintln(os.Stderr)

0 commit comments

Comments
 (0)