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. |     # 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: |     teams: | ||||||
|     - my-team |     - 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 ( | const ( | ||||||
| 	apiURL = "https://api.bitbucket.org/2.0" | 	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. | 	// Bitbucket requires this scope to access '/user' API endpoints. | ||||||
| 	scopeAccount = "account" | 	scopeAccount = "account" | ||||||
| 	// Bitbucket requires this scope to access '/user/emails' API endpoints. | 	// Bitbucket requires this scope to access '/user/emails' API endpoints. | ||||||
| @@ -33,21 +34,24 @@ const ( | |||||||
|  |  | ||||||
| // Config holds configuration options for Bitbucket logins. | // Config holds configuration options for Bitbucket logins. | ||||||
| type Config struct { | type Config struct { | ||||||
| 	ClientID     string   `json:"clientID"` | 	ClientID          string   `json:"clientID"` | ||||||
| 	ClientSecret string   `json:"clientSecret"` | 	ClientSecret      string   `json:"clientSecret"` | ||||||
| 	RedirectURI  string   `json:"redirectURI"` | 	RedirectURI       string   `json:"redirectURI"` | ||||||
| 	Teams        []string `json:"teams"` | 	Teams             []string `json:"teams"` | ||||||
|  | 	IncludeTeamGroups bool     `json:"includeTeamGroups,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Open returns a strategy for logging in through Bitbucket. | // 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{ | 	b := bitbucketConnector{ | ||||||
| 		redirectURI:  c.RedirectURI, | 		redirectURI:       c.RedirectURI, | ||||||
| 		teams:        c.Teams, | 		teams:             c.Teams, | ||||||
| 		clientID:     c.ClientID, | 		clientID:          c.ClientID, | ||||||
| 		clientSecret: c.ClientSecret, | 		clientSecret:      c.ClientSecret, | ||||||
| 		apiURL:       apiURL, | 		includeTeamGroups: c.IncludeTeamGroups, | ||||||
| 		logger:       logger, | 		apiURL:            apiURL, | ||||||
|  | 		legacyAPIURL:      legacyAPIURL, | ||||||
|  | 		logger:            logger, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &b, nil | 	return &b, nil | ||||||
| @@ -71,10 +75,13 @@ type bitbucketConnector struct { | |||||||
| 	clientSecret string | 	clientSecret string | ||||||
| 	logger       log.Logger | 	logger       log.Logger | ||||||
| 	apiURL       string | 	apiURL       string | ||||||
|  | 	legacyAPIURL string | ||||||
|  |  | ||||||
| 	// the following are used only for tests | 	// the following are used only for tests | ||||||
| 	hostName   string | 	hostName   string | ||||||
| 	httpClient *http.Client | 	httpClient *http.Client | ||||||
|  |  | ||||||
|  | 	includeTeamGroups bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // groupsRequired returns whether dex requires Bitbucket's 'team' scope. | // 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 | 	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 | // get creates a "GET `apiURL`" request with context, sends the request using | ||||||
| // the client, and decodes the resulting response body into v. | // the client, and decodes the resulting response body into v. | ||||||
| // Any errors encountered when building requests, sending requests, and | // Any errors encountered when building requests, sending requests, and | ||||||
|   | |||||||
| @@ -29,9 +29,12 @@ func TestUserGroups(t *testing.T) { | |||||||
|  |  | ||||||
| 	s := newTestServer(map[string]interface{}{ | 	s := newTestServer(map[string]interface{}{ | ||||||
| 		"/user/permissions/teams": teamsResponse, | 		"/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()) | 	groups, err := connector.userTeams(context.Background(), newClient()) | ||||||
|  |  | ||||||
| 	expectNil(t, err) | 	expectNil(t, err) | ||||||
| @@ -41,6 +44,19 @@ func TestUserGroups(t *testing.T) { | |||||||
| 		"team-3", | 		"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() | 	s.Close() | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user