diff --git a/Documentation/connectors/bitbucketcloud.md b/Documentation/connectors/bitbucketcloud.md index 2267079a..1e23bf30 100644 --- a/Documentation/connectors/bitbucketcloud.md +++ b/Documentation/connectors/bitbucketcloud.md @@ -31,4 +31,8 @@ connectors: # If `teams` is provided, this acts as a whitelist - only the user's Bitbucket teams that are in the configured `teams` below will go into the groups claim. Conversely, if the user is not in any of the configured `teams`, the user will not be authenticated. teams: - my-team + # Optional parameter to include team groups. + # If enabled, the groups claim of dex id_token will looks like this: + # ["my_team", "my_team/administrators", "my_team/members"] + includeTeamGroups: true ``` diff --git a/connector/bitbucketcloud/bitbucketcloud.go b/connector/bitbucketcloud/bitbucketcloud.go index ef942ae2..039084fe 100644 --- a/connector/bitbucketcloud/bitbucketcloud.go +++ b/connector/bitbucketcloud/bitbucketcloud.go @@ -21,7 +21,8 @@ import ( const ( apiURL = "https://api.bitbucket.org/2.0" - + // Switch to API v2.0 when the Atlassian platform services are fully available in Bitbucket + legacyAPIURL = "https://api.bitbucket.org/1.0" // Bitbucket requires this scope to access '/user' API endpoints. scopeAccount = "account" // Bitbucket requires this scope to access '/user/emails' API endpoints. @@ -33,21 +34,24 @@ const ( // Config holds configuration options for Bitbucket logins. type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - Teams []string `json:"teams"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + Teams []string `json:"teams"` + IncludeTeamGroups bool `json:"includeTeamGroups,omitempty"` } // Open returns a strategy for logging in through Bitbucket. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) { b := bitbucketConnector{ - redirectURI: c.RedirectURI, - teams: c.Teams, - clientID: c.ClientID, - clientSecret: c.ClientSecret, - apiURL: apiURL, - logger: logger, + redirectURI: c.RedirectURI, + teams: c.Teams, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + includeTeamGroups: c.IncludeTeamGroups, + apiURL: apiURL, + legacyAPIURL: legacyAPIURL, + logger: logger, } return &b, nil @@ -71,10 +75,13 @@ type bitbucketConnector struct { clientSecret string logger log.Logger apiURL string + legacyAPIURL string // the following are used only for tests hostName string httpClient *http.Client + + includeTeamGroups bool } // groupsRequired returns whether dex requires Bitbucket's 'team' scope. @@ -396,9 +403,39 @@ func (b *bitbucketConnector) userTeams(ctx context.Context, client *http.Client) } } + if b.includeTeamGroups { + for _, team := range teams { + teamGroups, err := b.userTeamGroups(ctx, client, team) + if err != nil { + return nil, fmt.Errorf("bitbucket: %v", err) + } + teams = append(teams, teamGroups...) + } + } + return teams, nil } +type group struct { + Slug string `json:"slug"` +} + +func (b *bitbucketConnector) userTeamGroups(ctx context.Context, client *http.Client, teamName string) ([]string, error) { + var teamGroups []string + apiURL := b.legacyAPIURL + "/groups/" + teamName + + var response []group + if err := get(ctx, client, apiURL, &response); err != nil { + return nil, fmt.Errorf("get user team %q groups: %v", teamName, err) + } + + for _, group := range response { + teamGroups = append(teamGroups, teamName+"/"+group.Slug) + } + + return teamGroups, nil +} + // get creates a "GET `apiURL`" request with context, sends the request using // the client, and decodes the resulting response body into v. // Any errors encountered when building requests, sending requests, and diff --git a/connector/bitbucketcloud/bitbucketcloud_test.go b/connector/bitbucketcloud/bitbucketcloud_test.go index c9990d48..3d984a8f 100644 --- a/connector/bitbucketcloud/bitbucketcloud_test.go +++ b/connector/bitbucketcloud/bitbucketcloud_test.go @@ -29,9 +29,12 @@ func TestUserGroups(t *testing.T) { s := newTestServer(map[string]interface{}{ "/user/permissions/teams": teamsResponse, + "/groups/team-1": []group{{Slug: "administrators"}, {Slug: "members"}}, + "/groups/team-2": []group{{Slug: "everyone"}}, + "/groups/team-3": []group{}, }) - connector := bitbucketConnector{apiURL: s.URL} + connector := bitbucketConnector{apiURL: s.URL, legacyAPIURL: s.URL} groups, err := connector.userTeams(context.Background(), newClient()) expectNil(t, err) @@ -41,6 +44,19 @@ func TestUserGroups(t *testing.T) { "team-3", }) + connector.includeTeamGroups = true + groups, err = connector.userTeams(context.Background(), newClient()) + + expectNil(t, err) + expectEquals(t, groups, []string{ + "team-1", + "team-2", + "team-3", + "team-1/administrators", + "team-1/members", + "team-2/everyone", + }) + s.Close() }