feat: Add team groups support to bitbucket connector
Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com>
This commit is contained in:
		| @@ -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 | ||||
| ``` | ||||
|   | ||||
| @@ -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. | ||||
| @@ -37,16 +38,19 @@ type Config struct { | ||||
| 	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, | ||||
| 		includeTeamGroups: c.IncludeTeamGroups, | ||||
| 		apiURL:            apiURL, | ||||
| 		legacyAPIURL:      legacyAPIURL, | ||||
| 		logger:            logger, | ||||
| 	} | ||||
|  | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user