Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2.9] GitHub Auth Provider: allow to search teams #45162

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 78 additions & 26 deletions pkg/auth/providers/github/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -37,27 +36,24 @@ func (g *GClient) getAccessToken(code string, config *v32.GithubConfig) (string,

b, err := g.postToGithub(url, form)
if err != nil {
logrus.Errorf("Github getAccessToken: GET url %v received error from github, err: %v", url, err)
return "", err
return "", fmt.Errorf("github getAccessToken: POST url %v received error from github, err: %v", url, err)
}

// Decode the response
var respMap map[string]interface{}

if err := json.Unmarshal(b, &respMap); err != nil {
logrus.Errorf("Github getAccessToken: received error unmarshalling response body, err: %v", err)
return "", err
return "", fmt.Errorf("github getAccessToken: received error unmarshalling response body, err: %v", err)
}

if respMap["error"] != nil {
desc := respMap["error_description"]
logrus.Errorf("Received Error from github %v, description from github %v", respMap["error"], desc)
return "", fmt.Errorf("Received Error from github %v, description from github %v", respMap["error"], desc)
return "", fmt.Errorf("github getAccessToken: received error from github %v, description from github %v", respMap["error"], desc)
}

acessToken, ok := respMap["access_token"].(string)
if !ok {
return "", fmt.Errorf("Received Error reading accessToken from response %v", respMap)
return "", fmt.Errorf("github getAccessToken: received error reading accessToken from response %v", respMap)
}
return acessToken, nil
}
Expand Down Expand Up @@ -118,11 +114,54 @@ func (g *GClient) getTeams(githubAccessToken string, config *v32.GithubConfig) (
logrus.Errorf("Github getGithubTeams: received error unmarshalling teams array, err: %v", err)
return teams, err
}
for _, teamObj := range teamObjs {
teams = append(teams, teamObj)
teams = append(teams, teamObjs...)

}
return teams, nil
}

// getOrgTeams returns the teams belonging to an organization.
func (g *GClient) getOrgTeams(githubAccessToken string, config *v32.GithubConfig, org Account) ([]Account, error) {
url := fmt.Sprintf(g.getURL("ORG_TEAMS", config), url.PathEscape(org.Login))
responses, err := g.paginateGithub(githubAccessToken, url)
if err != nil {
logrus.Errorf("Github getGithubTeams: GET url %v received error from github, err: %v", url, err)
return nil, err
}

var teams, respTeams []Account
pmatseykanets marked this conversation as resolved.
Show resolved Hide resolved
for _, response := range responses {
respTeams, err = g.getOrgTeamInfo(response, config, org)
if err != nil {
logrus.Errorf("Github getOrgTeams: received error unmarshalling teams array, err: %v", err)
return teams, err
}
teams = append(teams, respTeams...)
}

return teams, nil
}

// getOrgTeamInfo is similar to getTeamInfo but takes an org as an argument.
func (g *GClient) getOrgTeamInfo(b []byte, config *v32.GithubConfig, org Account) ([]Account, error) {
var teams []Account
var teamObjs []Team
if err := json.Unmarshal(b, &teamObjs); err != nil {
logrus.Errorf("Github getTeamInfo: received error unmarshalling team array, err: %v", err)
return teams, err
}

url := g.getURL("TEAM_PROFILE", config)
for _, team := range teamObjs {
teams = append(teams, Account{
ID: team.ID,
Name: team.Name,
AvatarURL: org.AvatarURL,
HTMLURL: fmt.Sprintf(url, org.Login, team.Slug),
Login: team.Slug,
})
}

return teams, nil
}

Expand Down Expand Up @@ -221,22 +260,34 @@ func (g *GClient) searchUsers(searchTerm, searchType string, githubAccessToken s
return result.Items, nil
}

func (g *GClient) getOrgByName(org string, githubAccessToken string, config *v32.GithubConfig) (Account, error) {
org = URLEncoded(org)
url := g.getURL("ORGS", config) + org

b, _, err := g.getFromGithub(githubAccessToken, url)
// searchTeams searches for teams that match the search term in the organizations the access token has access to.
// At the moment it only does a case-insensitive prefix match on the team's name.
func (g *GClient) searchTeams(searchTerm, githubAccessToken string, config *v32.GithubConfig) ([]Account, error) {
orgs, err := g.getOrgs(githubAccessToken, config)
if err != nil {
logrus.Debugf("Github getGithubOrgByName: GET url %v received error from github, err: %v", url, err)
return Account{}, err
return nil, err
}
var githubAcct Account
if err := json.Unmarshal(b, &githubAcct); err != nil {
logrus.Errorf("Github getGithubOrgByName: error unmarshalling response, err: %v", err)
return Account{}, err

lowerSearchTerm := strings.ToLower(searchTerm)

var matches, teams []Account
for _, org := range orgs {
teams, err = g.getOrgTeams(githubAccessToken, config, org)
if err != nil {
return nil, err
}

for _, team := range teams {
if !strings.HasPrefix(strings.ToLower(team.Name), lowerSearchTerm) {
continue
}

matches = append(matches, team)
}

}

return githubAcct, nil
return matches, nil
}

func (g *GClient) getUserOrgByID(id string, githubAccessToken string, config *v32.GithubConfig) (Account, error) {
Expand Down Expand Up @@ -289,10 +340,10 @@ func (g *GClient) postToGithub(url string, form url.Values) ([]byte, error) {
default:
var body bytes.Buffer
io.Copy(&body, resp.Body)
return nil, fmt.Errorf("Request failed, got status code: %d. Response: %s",
return nil, fmt.Errorf("request failed, got status code: %d. Response: %s",
resp.StatusCode, body.Bytes())
}
return ioutil.ReadAll(resp.Body)
return io.ReadAll(resp.Body)
}

func (g *GClient) getFromGithub(githubAccessToken string, url string) ([]byte, string, error) {
Expand Down Expand Up @@ -321,12 +372,11 @@ func (g *GClient) getFromGithub(githubAccessToken string, url string) ([]byte, s
}

nextURL := g.nextGithubPage(resp)
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
return b, nextURL, err
}

func (g *GClient) getURL(endpoint string, config *v32.GithubConfig) string {

var hostName, apiEndpoint, toReturn string

if config.Hostname != "" {
Expand Down Expand Up @@ -368,6 +418,8 @@ func (g *GClient) getURL(endpoint string, config *v32.GithubConfig) string {
toReturn = apiEndpoint + "/user/teams?per_page=100"
case "TEAM_PROFILE":
toReturn = hostName + "/orgs/%s/teams/%s"
case "ORG_TEAMS":
toReturn = apiEndpoint + "/orgs/%s/teams?per_page=100"
default:
toReturn = apiEndpoint
}
Expand Down
146 changes: 146 additions & 0 deletions pkg/auth/providers/github/github_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package github

import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
)

func TestGitHubClientGetOrgTeams(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
[{
"id": 9933605,
"name": "developers",
"slug": "developers"
}]`))
}))
defer srv.Close()

srvURL, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}

gcClient := &GClient{httpClient: srv.Client()}

config := &v32.GithubConfig{
Hostname: srvURL.Host,
}

org := Account{
ID: 9343010,
Login: "org",
AvatarURL: srvURL.Host + "/u/9343010/avatar",
}
teams, err := gcClient.getOrgTeams("", config, org)
if err != nil {
t.Fatal(err)
}

if want, got := 1, len(teams); want != got {
t.Fatalf("Expected teams %d got %d", want, got)
}

if want, got := 9933605, teams[0].ID; want != got {
t.Errorf("Expected ID %d got %d", want, got)
}
if want, got := "developers", teams[0].Login; want != got {
t.Errorf("Expected login %s got %s", want, got)
}
if want, got := "developers", teams[0].Name; want != got {
t.Errorf("Expected name %s got %s", want, got)
}
if want, got := org.AvatarURL, teams[0].AvatarURL; want != got {
t.Errorf("Expected avatarURL %s got %s", want, got)
}
if !strings.HasSuffix(teams[0].HTMLURL, "/orgs/org/teams/developers") {
t.Errorf("Unexpected htmlURL %s", teams[0].HTMLURL)
}
}

func TestGetUrlForOrgTeams(t *testing.T) {
var userOrgs, org1Teams, org2Teams []byte

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch path := r.URL.Path; path {
case "/api/v3/user/orgs":
w.Write(userOrgs)
case "/api/v3/orgs/org1/teams":
w.Write(org1Teams)
case "/api/v3/orgs/org2/teams":
w.Write(org2Teams)
default:
t.Errorf("Unexpected client call %s", path)
}
}))
defer srv.Close()

srvURL, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}

userOrgs = []byte(`
[{
"id": 9343010,
"login": "org1",
"avatar_url": "` + srvURL.Host + `/u/9343010/avatar"
},{
"id": 9343011,
"login": "org2",
"avatar_url": "` + srvURL.Host + `/u/9343011/avatar"
}]`)
org1Teams = []byte(`
[{
"id": 9933605,
"name": "developers",
"slug": "developers"
},{
"id": 9933606,
"name": "security",
"slug": "security"
}]`)
org2Teams = []byte(`
[{
"id": 9933607,
"name": "dev-ops",
"slug": "dev-ops"
}]`)

gcClient := &GClient{httpClient: srv.Client()}

config := &v32.GithubConfig{
Hostname: srvURL.Host,
}

teams, err := gcClient.searchTeams("dev", "", config)
if err != nil {
t.Fatal(err)
}

if want, got := 2, len(teams); want != got {
t.Fatalf("Expected teams %d got %d", want, got)
}

for _, team := range teams {
switch team.ID {
case 9933605, 9933607:
default:
t.Errorf("Unexpected team %d", team.ID)
}
}

teams, err = gcClient.searchTeams("foo", "", config)
if err != nil {
t.Fatal(err)
}

if want, got := 0, len(teams); want != got {
t.Fatalf("Expected teams %d got %d", want, got)
}
}
Loading
Loading