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.
|
# 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 a new issue