Skip to content

Commit

Permalink
[v2.9] GitHub Auth Provider: allow to search teams (#45162)
Browse files Browse the repository at this point in the history
Ref: #44907
  • Loading branch information
pmatseykanets committed May 3, 2024
1 parent 68e395e commit 052adb3
Show file tree
Hide file tree
Showing 5 changed files with 494 additions and 49 deletions.
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
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

0 comments on commit 052adb3

Please sign in to comment.