Merge pull request #1688 from flant/bitbucket-groups

feat: Add team groups support to bitbucket connector
This commit is contained in:
Márk Sági-Kazár 2020-10-04 20:08:49 +02:00 committed by GitHub
commit 828a1c6ec2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 70 additions and 13 deletions

View file

@ -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
``` ```

View file

@ -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.
@ -37,16 +38,19 @@ type Config struct {
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,
includeTeamGroups: c.IncludeTeamGroups,
apiURL: apiURL, apiURL: apiURL,
legacyAPIURL: legacyAPIURL,
logger: logger, logger: logger,
} }
@ -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

View file

@ -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()
} }