Merge pull request #1688 from flant/bitbucket-groups
feat: Add team groups support to bitbucket connector
This commit is contained in:
commit
828a1c6ec2
3 changed files with 70 additions and 13 deletions
|
@ -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.
|
||||
|
@ -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
|
||||
|
|
|
@ -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 a new issue