@@ -21,37 +21,44 @@ import (
2121
2222const (
2323 apiURL = "https://api.bitbucket.org/2.0"
24- // Switch to API v2.0 when the Atlassian platform services are fully available in Bitbucket
25- legacyAPIURL = "https://api.bitbucket.org/1.0"
2624 // Bitbucket requires this scope to access '/user' API endpoints.
2725 scopeAccount = "account"
2826 // Bitbucket requires this scope to access '/user/emails' API endpoints.
2927 scopeEmail = "email"
30- // Bitbucket requires this scope to access '/teams' API endpoints
31- // which are used when a client includes the 'groups' scope.
32- scopeTeams = "team"
3328)
3429
3530// Config holds configuration options for Bitbucket logins.
3631type Config struct {
37- ClientID string `json:"clientID"`
38- ClientSecret string `json:"clientSecret"`
39- RedirectURI string `json:"redirectURI"`
40- Teams []string `json:"teams"`
41- IncludeTeamGroups bool `json:"includeTeamGroups,omitempty"`
32+ ClientID string `json:"clientID"`
33+ ClientSecret string `json:"clientSecret"`
34+ RedirectURI string `json:"redirectURI"`
35+ Teams []string `json:"teams"`
36+
37+ // Deprecated: The Bitbucket 1.0 API (/1.0/groups/{team}) that this feature
38+ // relied on has been removed by Atlassian. This option is ignored; if set,
39+ // a warning is logged at startup. Consider using getWorkspacePermissions.
40+ IncludeTeamGroups bool `json:"includeTeamGroups,omitempty"`
41+
42+ // When enabled, appends workspace permission suffixes (e.g. "workspace:owner",
43+ // "workspace:member") to the groups claim, similar to GitLab's getGroupsPermission.
44+ GetWorkspacePermissions bool `json:"getWorkspacePermissions,omitempty"`
4245}
4346
4447// Open returns a strategy for logging in through Bitbucket.
4548func (c * Config ) Open (id string , logger * slog.Logger ) (connector.Connector , error ) {
49+ if c .IncludeTeamGroups {
50+ logger .Warn ("bitbucket: includeTeamGroups is deprecated and has no effect; " +
51+ "the Bitbucket 1.0 API it relied on has been removed by Atlassian" )
52+ }
53+
4654 b := bitbucketConnector {
47- redirectURI : c .RedirectURI ,
48- teams : c .Teams ,
49- clientID : c .ClientID ,
50- clientSecret : c .ClientSecret ,
51- includeTeamGroups : c .IncludeTeamGroups ,
52- apiURL : apiURL ,
53- legacyAPIURL : legacyAPIURL ,
54- logger : logger .With (slog .Group ("connector" , "type" , "bitbucketcloud" , "id" , id )),
55+ redirectURI : c .RedirectURI ,
56+ teams : c .Teams ,
57+ clientID : c .ClientID ,
58+ clientSecret : c .ClientSecret ,
59+ getWorkspacePermissions : c .GetWorkspacePermissions ,
60+ apiURL : apiURL ,
61+ logger : logger .With (slog .Group ("connector" , "type" , "bitbucketcloud" , "id" , id )),
5562 }
5663
5764 return & b , nil
@@ -69,31 +76,26 @@ var (
6976)
7077
7178type bitbucketConnector struct {
72- redirectURI string
73- teams []string
74- clientID string
75- clientSecret string
76- logger * slog.Logger
77- apiURL string
78- legacyAPIURL string
79+ redirectURI string
80+ teams []string
81+ clientID string
82+ clientSecret string
83+ logger * slog.Logger
84+ apiURL string
85+ getWorkspacePermissions bool
7986
8087 // the following are used only for tests
8188 hostName string
8289 httpClient * http.Client
83-
84- includeTeamGroups bool
8590}
8691
87- // groupsRequired returns whether dex requires Bitbucket's 'team' scope .
92+ // groupsRequired returns whether dex needs to fetch Bitbucket workspace membership .
8893func (b * bitbucketConnector ) groupsRequired (groupScope bool ) bool {
8994 return len (b .teams ) > 0 || groupScope
9095}
9196
9297func (b * bitbucketConnector ) oauth2Config (scopes connector.Scopes ) * oauth2.Config {
9398 bitbucketScopes := []string {scopeAccount , scopeEmail }
94- if b .groupsRequired (scopes .Groups ) {
95- bitbucketScopes = append (bitbucketScopes , scopeTeams )
96- }
9799
98100 endpoint := bitbucket .Endpoint
99101 if b .hostName != "" {
@@ -344,6 +346,7 @@ func (b *bitbucketConnector) userEmail(ctx context.Context, client *http.Client)
344346 if response .Next == nil {
345347 break
346348 }
349+ apiURL = * response .Next
347350 }
348351
349352 return "" , errors .New ("bitbucket: user has no confirmed, primary email" )
@@ -369,29 +372,33 @@ func (b *bitbucketConnector) getGroups(ctx context.Context, client *http.Client,
369372 return nil , nil
370373}
371374
372- type workspaceSlug struct {
375+ type workspaceRef struct {
373376 Slug string `json:"slug"`
374377}
375378
376- type workspace struct {
377- Workspace workspaceSlug `json:"workspace"`
379+ type workspaceAccess struct {
380+ Workspace workspaceRef `json:"workspace"`
378381}
379382
380- type userWorkspacesResponse struct {
383+ type workspacesResponse struct {
381384 pagedResponse
382- Values []workspace `json:"values"`
385+ Values []workspaceAccess `json:"values"`
386+ }
387+
388+ type workspacePermission struct {
389+ Permission string `json:"permission"`
383390}
384391
385392func (b * bitbucketConnector ) userWorkspaces (ctx context.Context , client * http.Client ) ([]string , error ) {
386393 var teams []string
387- apiURL := b .apiURL + "/user/permissions/ workspaces"
394+ apiURL := b .apiURL + "/user/workspaces"
388395
389396 for {
390- // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces /#api-workspaces-get
391- var response userWorkspacesResponse
397+ // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-user /#api-user -workspaces-get
398+ var response workspacesResponse
392399
393400 if err := get (ctx , client , apiURL , & response ); err != nil {
394- return nil , fmt .Errorf ("bitbucket: get user teams : %v" , err )
401+ return nil , fmt .Errorf ("bitbucket: get user workspaces : %v" , err )
395402 }
396403
397404 for _ , value := range response .Values {
@@ -401,39 +408,33 @@ func (b *bitbucketConnector) userWorkspaces(ctx context.Context, client *http.Cl
401408 if response .Next == nil {
402409 break
403410 }
411+ apiURL = * response .Next
404412 }
405413
406- if b .includeTeamGroups {
414+ if b .getWorkspacePermissions {
415+ var permissionGroups []string
407416 for _ , team := range teams {
408- teamGroups , err := b .userTeamGroups (ctx , client , team )
417+ perm , err := b .userWorkspacePermission (ctx , client , team )
409418 if err != nil {
410- return nil , fmt .Errorf ("bitbucket: %v" , err )
419+ b .logger .Warn ("bitbucket: failed to get permission for workspace, skipping permission suffix" ,
420+ "workspace" , team , "error" , err )
421+ continue
411422 }
412- teams = append (teams , teamGroups ... )
423+ permissionGroups = append (permissionGroups , team + ":" + perm )
413424 }
425+ teams = append (teams , permissionGroups ... )
414426 }
415427
416428 return teams , nil
417429}
418430
419- type group struct {
420- Slug string `json:"slug"`
421- }
422-
423- func (b * bitbucketConnector ) userTeamGroups (ctx context.Context , client * http.Client , teamName string ) ([]string , error ) {
424- apiURL := b .legacyAPIURL + "/groups/" + teamName
425-
426- var response []group
431+ func (b * bitbucketConnector ) userWorkspacePermission (ctx context.Context , client * http.Client , workspaceSlug string ) (string , error ) {
432+ apiURL := b .apiURL + "/user/workspaces/" + workspaceSlug + "/permission"
433+ var response workspacePermission
427434 if err := get (ctx , client , apiURL , & response ); err != nil {
428- return nil , fmt .Errorf ("get user team %q groups : %v" , teamName , err )
435+ return "" , fmt .Errorf ("get workspace %q permission : %v" , workspaceSlug , err )
429436 }
430-
431- teamGroups := make ([]string , 0 , len (response ))
432- for _ , group := range response {
433- teamGroups = append (teamGroups , teamName + "/" + group .Slug )
434- }
435-
436- return teamGroups , nil
437+ return response .Permission , nil
437438}
438439
439440// get creates a "GET `apiURL`" request with context, sends the request using
0 commit comments