diff --git a/connector/github/github.go b/connector/github/github.go index 831a8f13fa..434f23718e 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -325,6 +325,8 @@ func (c *githubConnector) getGroups(ctx context.Context, client *http.Client, gr return c.groupsForOrgs(ctx, client, userLogin) } else if c.org != "" { return c.teamsForOrg(ctx, client, c.org) + } else if groupScope { + return c.userGroups(ctx, client) } return nil, nil } @@ -572,10 +574,12 @@ func (c *githubConnector) userInOrg(ctx context.Context, client *http.Client, us // https://developer.github.com/v3/orgs/teams/#response-12 type team struct { Name string `json:"name"` - Org struct { - Login string `json:"login"` - } `json:"organization"` Slug string `json:"slug"` + Org org `json:"organization"` +} + +type org struct { + Login string `json:"login"` } // teamsForOrg queries the GitHub API for team membership within a specific organization. @@ -612,3 +616,76 @@ func (c *githubConnector) teamsForOrg(ctx context.Context, client *http.Client, return groups, nil } + +func (c *githubConnector) userGroups(ctx context.Context, client *http.Client) (groups []string, err error) { + + orgs, err := c.userOrgs(ctx, client) + if err != nil { + return groups, err + } + + orgTeams, err := c.userOrgTeams(ctx, client) + if err != nil { + return groups, err + } + + for _, org := range orgs { + if teams, ok := orgTeams[org]; !ok { + groups = append(groups, org) + } else { + for _, team := range teams { + groups = append(groups, org+":"+team) + } + } + } + + return groups, err +} + +func (c *githubConnector) userOrgs(ctx context.Context, client *http.Client) ([]string, error) { + apiURL, groups := c.apiURL+"/user/orgs", []string{} + for { + // https://developer.github.com/v3/orgs/#list-your-organizations + var ( + orgs []org + err error + ) + if apiURL, err = get(ctx, client, apiURL, &orgs); err != nil { + return nil, fmt.Errorf("github: get orgs: %v", err) + } + + for _, org := range orgs { + groups = append(groups, org.Login) + } + + if apiURL == "" { + break + } + } + + return groups, nil +} + +func (c *githubConnector) userOrgTeams(ctx context.Context, client *http.Client) (map[string][]string, error) { + apiURL, groups := c.apiURL+"/user/teams", map[string][]string{} + for { + // https://developer.github.com/v3/orgs/teams/#list-user-teams + var ( + teams []team + err error + ) + if apiURL, err = get(ctx, client, apiURL, &teams); err != nil { + return nil, fmt.Errorf("github: get teams: %v", err) + } + + for _, team := range teams { + groups[team.Org.Login] = append(groups[team.Org.Login], team.Name) + } + + if apiURL == "" { + break + } + } + + return groups, nil +} diff --git a/connector/github/github_test.go b/connector/github/github_test.go new file mode 100644 index 0000000000..57aef55427 --- /dev/null +++ b/connector/github/github_test.go @@ -0,0 +1,121 @@ +package github + +import ( + "context" + "crypto/tls" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + "github.com/dexidp/dex/connector" +) + +func TestUserGroups(t *testing.T) { + + orgs := []org{ + {Login: "org-1"}, + {Login: "org-2"}, + {Login: "org-3"}, + } + + teams := []team{ + {Name: "team-1", Org: org{Login: "org-1"}}, + {Name: "team-2", Org: org{Login: "org-1"}}, + {Name: "team-3", Org: org{Login: "org-1"}}, + {Name: "team-4", Org: org{Login: "org-2"}}, + } + + s := newTestServer(map[string]interface{}{ + "/user/orgs": orgs, + "/user/teams": teams, + }) + + connector := githubConnector{apiURL: s.URL} + groups, err := connector.userGroups(context.Background(), newClient()) + + expectNil(t, err) + expectEquals(t, groups, []string{ + "org-1:team-1", + "org-1:team-2", + "org-1:team-3", + "org-2:team-4", + "org-3", + }) + + s.Close() +} + +func TestUserGroupsWithoutOrgs(t *testing.T) { + + s := newTestServer(map[string]interface{}{ + "/user/orgs": []org{}, + "/user/teams": []team{}, + }) + + connector := githubConnector{apiURL: s.URL} + groups, err := connector.userGroups(context.Background(), newClient()) + + expectNil(t, err) + expectEquals(t, len(groups), 0) + + s.Close() +} + +func TestUsernameIncludedInFederatedIdentity(t *testing.T) { + + s := newTestServer(map[string]interface{}{ + "/user": user{Login: "some-login"}, + "/user/emails": []userEmail{{ + Email: "some@email.com", + Verified: true, + Primary: true, + }}, + "/login/oauth/access_token": map[string]interface{}{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", + "expires_in": "30", + }, + }) + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + req, err := http.NewRequest("GET", hostURL.String(), nil) + expectNil(t, err) + + githubConnector := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient()} + identity, err := githubConnector.HandleCallback(connector.Scopes{}, req) + + expectNil(t, err) + expectEquals(t, identity.Username, "some-login") + + s.Close() +} + +func newTestServer(responses map[string]interface{}) *httptest.Server { + return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(responses[r.URL.Path]) + })) +} + +func newClient() *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + return &http.Client{Transport: tr} +} + +func expectNil(t *testing.T, a interface{}) { + if a != nil { + t.Errorf("Expected %+v to equal nil", a) + } +} + +func expectEquals(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Errorf("Expected %+v to equal %+v", a, b) + } +}